使用GCC在C中的函数重载 – 具有多个参数的函数

在前面的一个问题中,当每个函数只接受一个参数时,我找到了一种在C99中重载函数的方法。 请参阅以下内容中的答案: C中的函数重载使用GCC – 编译器警告以获取详细信息。

现在我已经找到了使用单个参数函数的方法,我想知道如何对带有多个参数的函数进行此操作。 我假设它将与__VA_ARGS__和使用...但我似乎无法找到任何有效甚至想要编译的东西。


这适用于带有2个参数的打印:

 #define print(x, y) \ __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), int) && \ __builtin_types_compatible_p(typeof(y), int), print_int, \ (void)0)(x, y) 

但如果我还想要另一个带有一个参数的版本,我就无法重新定义它。 添加这将给我一个错误,说重新定义print

 #define print(x) \ __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), char[]), print_string, \ (void)0)(x) 

如何重载打印,以便它可以使用2个整数作为输入或字符数组?

示例用法:

 print(1, 2); print("this"); 

或者甚至更好……我怎样才能使它与任何类型的组合或任意数量的参数一起使用?

还要记住,因为这是C99,_Generic关键字不可用。

你可以使用GCC的扩展和过量的预处理器技巧来做你想要的。 评论者已经明确表达了他们的观点:C非常明确,与所产生的符号具有一对一的关系。 如果要进行函数重载和类型检查,请使用提供它们的多种语言之一。

巴洛克式的宏观解决方案往往是玩具而不是适合生产的代码,但它仍然是一个有趣的练习。 然而,安全问题仍然存在,并且要注意:

  • …解决方案不可移植,因为通过类型选择参数的核心噱头已经是GCC特定的。
  • …解决方案是基于宏构建的。 在宏中查找语法错误很困难,因为错误消息是指用户看不到的扩展代码。
  • …解决方案使用许多宏名称污染名称空间。 如果您真的想要使用此解决方案,请将所有宏(除了最明显的宏)前缀一致,以尽量减少符号冲突的危险。

这样,让我们​​实现一个函数put ,根据其类型将其参数写入stdin

 const char *name = "Fred"; double C = 12.5; put(1, " ", 2); // 1 2 put("Hello, I'm ", name, "!"); // Hello, I'm Fred! put(C, " Celsius"); // 12.5 Celsius put(C * 1.8 + 32.0, " Fahrenheit"); // 54.5 Fahrenheit 

为简单起见,该解决方案最多只接受intconst char *double三个参数,但最大参数数是可扩展的。

解决方案包括以下部分:

变exception数型宏

假设您想拥有一个对所有参数求和的函数。 参数的数量可能不同,但所有参数都是double类型。 如果它们不是double类型,它们应该被提升为double

变量函数不是一个好的解决方案,因为它们会将参数传递给每个类型的函数。 试图将sum(1, 2, 3)会产生灾难性的后果。

相反,您可以使用复合文字来动态创建double的数组。 使用sizeof机制来获取数组的长度。 (参数可能有副作用,因为不评估sizeof内的数组,只确定其大小。)

 #define sum(...) sum_impl(sizeof((double[]){__VA_ARGS__})/ \ sizeof(double), (double[]){__VA_ARGS__}) double sum_impl(size_t n, double x[]) { double s = 0.0; while (n--) s += x[n]; return s; } 

对于在double s上执行的计算sum(1, 2, 3)这将对sum(1, 2, 3)产生6.0

变体类型

您希望所有参数都具有相同的类型,但此类型应该能够表示函数的所有受支持类型。 创建变体的C方法是使用标记联合, struct内部的union

 typedef struct var_t var_t; struct var_t { int type; union { int i; double f; const char *s; } data; }; 

类型可以是枚举。 我在这里使用printf格式的charcter常量。

表达式的变体由宏VAR确定,它基本上是您在上面发布的gcc:

 #define CHOOSE __builtin_choose_expr #define IFTYPE(X, T) __builtin_types_compatible_p(typeof(X), T) #define VAR(X) \ CHOOSE(IFTYPE(X, int), make_var_i, \ CHOOSE(IFTYPE(X, const char[]), make_var_s, \ CHOOSE(IFTYPE(X, const char *), make_var_s, \ CHOOSE(IFTYPE(X, double), make_var_f, \ make_var_0))))(X) 

宏调用任何make_var函数。 必须为每个有效类型定义这些函数:

 var_t make_var_i(int X) { var_t v = {'i', {.i = X}}; return v; } var_t make_var_s(const char *X) { var_t v = {'s', {.s = X}}; return v; } var_t make_var_f(double X) { var_t v = {'f', {.f = X}}; return v; } var_t make_var_0() { var_t v = {'#'}; return v; } 

正如您已经发现的那样,将X合并到依赖于类型的表达式中是行不通的。 你也不能在这里使用复合文字与指定的初始化者,可能是出于同样的原因。 (我说用宏检查错误很难,不是吗?)

这是GCC唯一的具体部分; 它也可以用C11的_Generic实现。

将宏应用于函数的所有参数

必须将VAR宏应用于可变参数put宏的所有参数。 在得到一个空列表之前,你不能处理可变参数的头部,因为你不能递归地扩展宏,但是你可以使用一个技巧来计算宏的参数,然后扩展到一个带有那么多参数的宏:

 #define PUT1(_1) put_impl(1, (var_t[]){VAR(_1)}) #define PUT2(_1, _2) put_impl(2, (var_t[]){VAR(_1), VAR(_2)}) #define PUT3(_1, _2, _3) put_impl(3, (var_t[]){VAR(_1), VAR(_2), VAR(_3)}) #define SELECT_N(_1, _2, _3, N, ...) N #define put(...) SELECT_N(__VA_ARGS__, PUT3, PUT2, PUT1)(__VA_ARGS__) 

现在put需要1个,2个或3个参数。 如果提供的值超过3,则会收到一条模糊的错误消息,该消息与不提供太多参数无关。

上面的代码不接受空参数列表。 使用GCC entension , ##__VA_ARGS ,只有当variadicargument列表不为空时才会写逗号,你可以将它扩展为:

 #define PUT0() put_impl(0, NULL) #define PUT1(_1) put_impl(1, (var_t[]){VAR(_1)}) #define PUT2(_1, _2) put_impl(2, (var_t[]){VAR(_1), VAR(_2)}) #define PUT3(_1, _2, _3) put_impl(3, (var_t[]){VAR(_1), VAR(_2), VAR(_3)}) #define SELECT_N(X, _1, _2, _3, N, ...) N #define put(...) SELECT_N(X, ##__VA_ARGS__, PUT3, PUT2, PUT1,PUT0)(__VA_ARGS__) 

如果您愿意,可以将此解决方案扩展到任意多个参数。

实施

上面的宏调用了put_impl函数,它是如何打印n变体数组的实现。 在完成上述所有技巧之后,function相当简单:

 void put_impl(size_t n, const var_t var[]) { for (size_t i = 0; i < n; i++) { switch(var[i].type) { case 'i': printf("%i", var[i].data.i); break; case 'f': printf("%g", var[i].data.f); break; case 's': printf("%s", var[i].data.s); break; case '#': printf("[undef]"); break; } } putchar('\n'); } 

把它们放在一起

以下程序使用上述方法打印一些相当愚蠢的东西。 它不可移植,但如果使用gcc -std=gnu99编译gcc -std=gnu99

 #include  #include  #define CHOOSE __builtin_choose_expr #define IFTYPE(X, T) __builtin_types_compatible_p(typeof(X), T) #define VAR(X) \ CHOOSE(IFTYPE(X, int), make_var_i, \ CHOOSE(IFTYPE(X, const char[]), make_var_s, \ CHOOSE(IFTYPE(X, const char *), make_var_s, \ CHOOSE(IFTYPE(X, double), make_var_f, \ make_var_0))))(X) #define PUT0() put_impl(0, NULL) #define PUT1(_1) put_impl(1, (var_t[]){VAR(_1)}) #define PUT2(_1, _2) put_impl(2, (var_t[]){VAR(_1), VAR(_2)}) #define PUT3(_1, _2, _3) put_impl(3, (var_t[]){VAR(_1), VAR(_2), VAR(_3)}) #define SELECT_N(X, _1, _2, _3, N, ...) N #define put(...) SELECT_N(X, ##__VA_ARGS__, PUT3, PUT2, PUT1,PUT0)(__VA_ARGS__) typedef struct var_t var_t; struct var_t { int type; union { int i; double f; const char *s; } data; }; var_t make_var_i(int X) { var_t v = {'i', {.i = X}}; return v; } var_t make_var_s(const char *X) { var_t v = {'s', {.s = X}}; return v; } var_t make_var_f(double X) { var_t v = {'f', {.f = X}}; return v; } var_t make_var_0() { var_t v = {'#'}; return v; } void put_impl(size_t n, const var_t var[]) { for (size_t i = 0; i < n; i++) { switch(var[i].type) { case 'i': printf("%i", var[i].data.i); break; case 'f': printf("%g", var[i].data.f); break; case 's': printf("%s", var[i].data.s); break; case '#': printf("[undef]"); break; } } putchar('\n'); } int main() { const char *name = "Fred"; double C = 12.5; put(1, " ", 2); put("Hello, I'm ", name, "!"); put(); put(C, " Celsius"); put(C * 1.8 + 32.0, " Fahrenheit"); return 0; } 

你可以对你想要支持的参数的类型和数量感到疯狂,但要记住,你的丛林越大,维护和调试就越困难。

这个解决方案绝不是通用的,但是它将为问题中提到的非常具体的案例完成工作。

 #include  #define print(...) \ __builtin_choose_expr(__builtin_types_compatible_p(typeof(FIRST(__VA_ARGS__)), int), print_int, print_string)\ (__VA_ARGS__) #define FIRST(A, ...) A void print_int(int i, int j) { printf("int: %d %d\n", i, j); } void print_string(char* s) { printf("char*: %s\n", s); } int main(int argc, char* argv[]) { print(1, 2); print("this"); return 0; } 

如果有人能够找到一个更通用的解决方案,当添加新的重载时,这个解决方案将始终如一地工作,这将非常受欢迎