使用gcc在C中键入typesafe varargs
很多时候我想要一个函数来接收可变数量的参数,例如以NULL结尾
#define push(stack_t stack, ...) _push(__VARARG__, NULL); func _push(stack_t stack, char *s, ...) { va_list args; va_start(args, s); while (s = va_arg(args, char*)) push_single(stack, s); }
如果foo收到非char*
变量,我可以指示gcc或clang警告吗? 类似于__attribute__(format)
,但是对于同一指针类型的多个参数。
我知道你想要以某种方式使用__attribute__((sentinel))
,但这是一个红色的鲱鱼。
你想要的是做这样的事情:
#define push(s, args...) ({ \ char *_args[] = {args}; \ _push(s,_args,sizeof(_args)/sizeof(char*)); \ })
包装:
void _push(stack_t s, char *args[], int argn);
您可以按照您希望的方式编写它!
然后你可以打电话:
push(stack, "foo", "bar", "baz"); push(stack, "quux");
我只能想到这样的事情:
#include #include #include typedef struct tArg { const char* Str; struct tArg* Next; } tArg; tArg* Arg(const char* str, tArg* nextArg) { tArg* p = malloc(sizeof(tArg)); if (p != NULL) { p->Str = str; p->Next = nextArg; } else { while (nextArg != NULL) { p = nextArg->Next; free(nextArg); nextArg = p; } } return p; } void PrintR(tArg* arg) { while (arg != NULL) { tArg* p; printf("%s", arg->Str); p = arg->Next; free(arg); arg = p; } } void (*(*(*(*(*(*(*Print8 (const char* Str)) (const char*)) (const char*)) (const char*)) (const char*)) (const char*)) (const char*)) (const char*) { printf("%s", Str); // There's probably a UB here: return (void(*(*(*(*(*(*(*) (const char*)) (const char*)) (const char*)) (const char*)) (const char*)) (const char*)) (const char*))&Print8; } int main(void) { PrintR(Arg("HELLO", Arg(" ", Arg("WORLD", Arg("!", Arg("\n", NULL)))))); // PrintR(Arg(1, NULL)); // warning/error // PrintR(Arg(&main, NULL)); // warning/error // PrintR(Arg(0, NULL)); // no warning/error // PrintR(Arg((void*)1, NULL)); // no warning/error Print8("hello")(" ")("world")("!")("\n"); // Same warning/error compilation behavior as with PrintR() return 0; }
C variadics的问题在于它们之后真的被拴在一起,而不是真正设计成语言。 主要问题是可变参数是匿名的,它们没有句柄,没有标识符。 这导致笨重的VA宏生成对没有名称的参数的引用。 它还需要告诉那些可变参数列表开始的宏以及预期参数的类型。
所有这些信息确实应该用语言本身的正确语法编码。
例如,可以在省略号之后用forms参数扩展现有的C语法,就像这样
void foo ( ... int counter, float arglist );
按照惯例,第一个参数可以是参数计数,第二个参数可以是参数列表。 在函数体内,列表可以在语法上作为数组处理。
通过这种约定,可变参数将不再是匿名的。 在函数体内,可以像任何其他参数一样引用计数器,并且可以引用列表元素,就好像它们是数组参数的数组元素一样,如此
void foo ( ... int counter, float arglist ) { unsigned i; for (i=0; i
通过语言本身内置的这种function,每次对arglist[i]
引用都将被转换为堆栈帧上的相应地址。 没有必要通过宏来做到这一点。
此外,编译器会自动插入参数计数,从而进一步减少出错的机会。
打电话给
foo(1.23, 4.56, 7.89);
将被编译为好像已经写好了
foo(3, 1.23, 4.56, 7.89);
在函数体内,可以在运行时检查超出实际传递的实际参数数量的元素的任何访问,并导致编译时故障,从而大大增强安全性。
最后但并非最不重要的是,所有的可变参数都是类型化的,并且可以在编译时进行类型检查,就像检查非可变参数一样。
在一些使用情况中,当然希望具有交替类型,例如在编写用于存储集合中的键和值的函数时。 这也可以简单地通过在省略号之后允许更正式的参数来适应,就像这样
void store ( collection dict, ... int counter, key_t key, val_t value );
然后可以将此函数称为
store(dict, key1, val1, key2, val2, key3, val3);
但会被编译成好像已经写好了
store(dict, 3, key1, val1, key2, val2, key3, val3);
实际参数的类型将根据相应的可变参数forms参数进行编译时检查。
在函数体内,计数器将再次被其标识符引用,键和值将被引用,就像它们是数组一样,
key[i]
是指第i个键/值对value[i]
键value[i]
是指第i个值对的值
并且这些引用将被编译到堆栈帧上它们各自的地址。
这些都不是真的很难做到,也从来没有。 但是,C的设计理念根本不利于这些function。
如果没有冒险的C编译器实现者(或C预处理器实现者)带头实现这个或类似的方案,我们不可能在C中看到任何这种类型。
麻烦的是,那些对类型安全感兴趣并且愿意投入工作来构建自己的编译器的人通常会得出这样的结论:C语言已经超越了打捞,人们也可以从更好的设计语言开始。
我自己一直在那里,最终决定放弃尝试,然后实施Wirth的一种语言,并为此添加了类型安全的可变参数。 此后我遇到了其他人,他们告诉我他们自己的流产企图。 C中的适当类型安全可变参数似乎仍然难以捉摸。