9

Tôi muốn tạo một con trỏ hàm tới một hàm sẽ xử lý một tập hợp con các trường hợp cho một hàm nhận danh sách tham số biến. Trường hợp sử dụng đang truyền một hàm mất ... đến một hàm có danh sách thông số cụ thể, do đó bạn có thể xử lý các thông số biến mà không phải xử lý va_list và bạn bè.Trong C, có bao giờ an toàn khi đúc con trỏ hàm variadic đến một con trỏ hàm với các đối số hữu hạn không?

Trong mã ví dụ sau, tôi đang truyền hàm có tham số biến cho hàm có danh sách tham số được mã hóa cứng (và ngược lại). Điều này làm việc (hoặc xảy ra để làm việc), nhưng tôi không biết nếu là một sự trùng hợp do quy ước gọi đang sử dụng. (Tôi đã thử nó trên hai nền tảng dựa trên x86_64 khác nhau.)

#include <stdio.h> 
#include <stdarg.h> 

void function1(char* s, ...) 
{ 
    va_list ap; 
    int tmp; 

    va_start(ap, s); 
    tmp = va_arg(ap, int); 
    printf(s, tmp); 
    va_end(ap); 
} 

void function2(char* s, int d) 
{ 
    printf(s, d); 
} 

typedef void (*functionX_t)(char*, int); 
typedef void (*functionY_t)(char*, ...); 

int main(int argc, char* argv[]) 
{ 
    int x = 42; 

    /* swap! */ 
    functionX_t functionX = (functionX_t) function1; 
    functionY_t functionY = (functionY_t) function2; 

    function1("%d\n", x); 
    function2("%d\n", x); 
    functionX("%d\n", x); 
    functionY("%d\n", x); 

    return 0; 
} 

Hành vi không xác định này? Nếu có, bất cứ ai có thể đưa ra một ví dụ về một nền tảng mà điều này sẽ không làm việc, hoặc một cách để tinh chỉnh ví dụ của tôi theo cách nó sẽ thất bại, cho một trường hợp sử dụng phức tạp hơn?


Sửa: Để giải quyết những ngụ ý rằng đoạn mã này sẽ phá vỡ với lý lẽ phức tạp hơn, tôi mở rộng ví dụ của tôi:

#include <stdio.h> 
#include <stdarg.h> 

struct crazy 
{ 
    float f; 
    double lf; 
    int d; 
    unsigned int ua[2]; 
    char* s; 
}; 

void function1(char* s, ...) 
{ 
    va_list ap; 
    struct crazy c; 

    va_start(ap, s); 
    c = va_arg(ap, struct crazy); 
    printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]); 
    va_end(ap); 
} 

void function2(char* s, struct crazy c) 
{ 
    printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]); 
} 

typedef void (*functionX_t)(char*, struct crazy); 
typedef void (*functionY_t)(char*, ...); 

int main(int argc, char* argv[]) 
{ 
    struct crazy c = 
    { 
     .f = 3.14, 
     .lf = 3.1415, 
     .d = -42, 
     .ua = { 0, 42 }, 
     .s = "this is crazy" 
    }; 


    /* swap! */ 
    functionX_t functionX = (functionX_t) function1; 
    functionY_t functionY = (functionY_t) function2; 

    function1("%s %f %lf %d %u %u\n", c); 
    function2("%s %f %lf %d %u %u\n", c); 
    functionX("%s %f %lf %d %u %u\n", c); 
    functionY("%s %f %lf %d %u %u\n", c); 

    return 0; 
} 

Nó vẫn hoạt động. Bất cứ ai có thể chỉ ra một ví dụ cụ thể khi điều này sẽ thất bại?

$ gcc -Wall -g -o varargs -O9 varargs.c 
$ ./varargs 
this is crazy 3.140000 3.141500 -42 0 42 
this is crazy 3.140000 3.141500 -42 0 42 
this is crazy 3.140000 3.141500 -42 0 42 
this is crazy 3.140000 3.141500 -42 0 42 
+2

"là hành vi không xác định điều này?" Vâng. Gọi một hàm thông qua một con trỏ hàm không tương thích luôn mang lại hành vi không xác định. –

+0

Dường như với tôi (ít nhất là bằng ví dụ) rằng các thông số đẩy vào ngăn xếp được thực hiện một cách nhất quán bất kể nếu một cuộc gọi variadic được sử dụng hay không. Vì vậy, trong khi nó thực sự có thể là "không xác định" cho mỗi spec (hoặc có thể chỉ không được đề cập?), Là hành vi bao giờ thực sự không phù hợp? – mpontillo

+3

@Mike: Các tham số Non-variadic được truyền qua thanh ghi CPU bất cứ khi nào có thể (ít nhất là trên x86). Họ không được đẩy vào ngăn xếp cả. Đây là một lý do tại sao nó không hoạt động. – AnT

Trả lời

8

Đúc con trỏ đến loại con trỏ hàm khác hoàn toàn an toàn. Nhưng điều duy nhất mà ngôn ngữ đảm bảo là bạn có thể đưa nó trở lại kiểu gốc và lấy giá trị con trỏ ban đầu.

Gọi hàm chức năng thông qua con trỏ được chuyển đổi mạnh thành loại con trỏ chức năng không tương thích dẫn đến hành vi không xác định. Điều này áp dụng cho tất cả các kiểu con trỏ hàm không tương thích, bất kể chúng có phải là variadic hay không.

Mã bạn đã đăng tạo ra hành vi không xác định: không phải tại điểm của diễn viên, nhưng tại thời điểm cuộc gọi.


Đang cố gắng để đuổi theo ví dụ "nơi nó sẽ thất bại" là một nỗ lực vô nghĩa, nhưng nó phải là dễ dàng dù sao, kể từ khi công ước truyền thông số (cả ở mức độ thấp và ngôn ngữ cấp) lại khác hẳn.Ví dụ, đoạn code dưới đây thường sẽ không "làm việc" trong thực tế

void foo(const char *s, float f) { printf(s, f); } 

int main() { 
    typedef void (*T)(const char *s, ...); 
    T p = (T) foo; 
    float f = 0.5; 
    p("%f\n", f); 
} 

Prints số không thay vì 0.5 (GCC)

+0

Mã ví dụ trong câu hỏi rõ ràng đang gọi hàm. Chỉ cần đúc con trỏ trong bối cảnh này sẽ không có ý nghĩa nhiều lắm, tôi sợ. – Mastermnd

+0

Ví dụ hay, cảm ơn! Ví dụ của bạn in '0.500000' cho tôi trên OS X, nhưng' 0.000000' trên Linux (x64). – mpontillo

4

Có, đây là hành vi không xác định.

Nó chỉ xảy ra để làm việc vì các con trỏ đang xếp hàng cho trình biên dịch, nền tảng và các kiểu tham số hiện tại của bạn. Hãy thử làm điều này với đôi và các loại khác và bạn có thể sẽ có thể tái tạo hành vi kỳ lạ.

Thậm chí nếu bạn không làm như vậy, đây là mã rất nguy hiểm.

Tôi giả sử bạn đang bực bội bởi các varargs. Xem xét việc xác định một tập hợp thông số chung trong một liên kết hoặc cấu trúc, và sau đó chuyển nó.

+1

Trên thực tế không chỉ khó chịu; đó là trường hợp tôi có một API không có phiên bản 'v' của cùng một hàm. Ví dụ, nếu nó là một cái gì đó giống như 'printf' luôn có' vprintf', nhưng chúng không có các hàm tương đương mà lấy một 'va_list'. Tôi không sở hữu API đó, vì vậy tôi không thể chỉ định cấu trúc và chuyển nó vào. – mpontillo

+2

Thật không may, trong trường hợp đó, tuyến đường an toàn là tạo trình bao bọc cho mỗi cuộc gọi và phân tích cú pháp varargs – Mastermnd

Các vấn đề liên quan