使用 GCC 在 C 中进行函数重载 - 具有多个参数的函数
Function overloading in C using GCC - functions with mutiple arguments
在上一个问题中,我找到了一种在 C99 中重载函数的方法,当每个函数只接受一个参数时。有关详细信息,请参阅 中的答案。
既然我已经找到了一种使用单参数函数的方法,我想知道如何为采用多个参数的函数完成此操作。我认为它与 __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
is redefined:
#define print(x) \
__builtin_choose_expr(__builtin_types_compatible_p(typeof(x), char[]), print_string, \
(void)0)(x)
如何重载 print 以便它可以使用 2 个整数作为输入或使用字符数组?
用法示例:
print(1, 2);
print("this");
甚至更好...我怎样才能使它适用于任意类型组合或任意数量的参数?
还要记住,因为这是 C99,所以 _Generic 关键字不可用。
此解决方案绝不是通用的,但它可以针对问题中提出的非常具体的情况完成工作。
#include <stdio.h>
#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;
}
如果有人能找到更通用的解决方案,并且在添加新重载时能够始终如一地工作,我们将不胜感激。
您可以使用 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
为了简单起见,该解决方案最多只接受 int
、const char *
或 double
三个参数,但参数的最大数量是可扩展的。
解决方案包括以下部分:
可变常量类型宏
假设您想要一个对所有参数求和的函数。参数的数量可能会有所不同,但所有参数的类型都是 double
。如果它们不是 double
类型,则应提升为 double
.
可变参数函数不是一个好的解决方案,因为它们会将参数传递给每个单独类型的函数。尝试 sum(1, 2, 3)
为 double
将导致灾难性的后果。
相反,您可以使用复合文字即时创建一个 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)
产生 6.0
。
变体类型
您希望所有参数都是同一类型,但该类型应该能够代表您的函数的所有支持类型。创建变体的 C 方法是使用标记联合,union
内 struct
:
typedef struct var_t var_t;
struct var_t {
int type;
union {
int i;
double f;
const char *s;
} data;
};
类型可以是枚举。我在这里根据 printf
格式使用字符常量。
表达式的变体由宏 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
变体数组的实现。经过上面所有的技巧,函数就相当简单了:
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
编译则运行:
#include <stdlib.h>
#include <stdio.h>
#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;
}
您可以对要支持的参数类型和数量着迷,但请记住,您的宏丛林越大,维护和调试就越困难。
在上一个问题中,我找到了一种在 C99 中重载函数的方法,当每个函数只接受一个参数时。有关详细信息,请参阅
既然我已经找到了一种使用单参数函数的方法,我想知道如何为采用多个参数的函数完成此操作。我认为它与 __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
is redefined:
#define print(x) \
__builtin_choose_expr(__builtin_types_compatible_p(typeof(x), char[]), print_string, \
(void)0)(x)
如何重载 print 以便它可以使用 2 个整数作为输入或使用字符数组?
用法示例:
print(1, 2);
print("this");
甚至更好...我怎样才能使它适用于任意类型组合或任意数量的参数?
还要记住,因为这是 C99,所以 _Generic 关键字不可用。
此解决方案绝不是通用的,但它可以针对问题中提出的非常具体的情况完成工作。
#include <stdio.h>
#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;
}
如果有人能找到更通用的解决方案,并且在添加新重载时能够始终如一地工作,我们将不胜感激。
您可以使用 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
为了简单起见,该解决方案最多只接受 int
、const char *
或 double
三个参数,但参数的最大数量是可扩展的。
解决方案包括以下部分:
可变常量类型宏
假设您想要一个对所有参数求和的函数。参数的数量可能会有所不同,但所有参数的类型都是 double
。如果它们不是 double
类型,则应提升为 double
.
可变参数函数不是一个好的解决方案,因为它们会将参数传递给每个单独类型的函数。尝试 sum(1, 2, 3)
为 double
将导致灾难性的后果。
相反,您可以使用复合文字即时创建一个 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)
产生 6.0
。
变体类型
您希望所有参数都是同一类型,但该类型应该能够代表您的函数的所有支持类型。创建变体的 C 方法是使用标记联合,union
内 struct
:
typedef struct var_t var_t;
struct var_t {
int type;
union {
int i;
double f;
const char *s;
} data;
};
类型可以是枚举。我在这里根据 printf
格式使用字符常量。
表达式的变体由宏 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
变体数组的实现。经过上面所有的技巧,函数就相当简单了:
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
编译则运行:
#include <stdlib.h>
#include <stdio.h>
#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;
}
您可以对要支持的参数类型和数量着迷,但请记住,您的宏丛林越大,维护和调试就越困难。