您可以使用 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 将产生灾难性的结果。
相反,您可以使用复合文字来动态创建doubles 数组。使用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;
}
这将在对doubles 执行的计算中为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 扩展 , ##__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;
}
您可以对要支持的参数的类型和数量发疯,但请记住,您的宏丛林越大,维护和调试就越困难。