C 可变参数的问题在于,它们实际上是在事后才用螺栓固定的,而不是真正设计到语言中的。主要问题是可变参数是匿名的,它们没有句柄,没有标识符。这导致笨拙的 VA 宏生成对没有名称的参数的引用。它还导致需要告诉那些宏变量列表从哪里开始以及参数应该是什么类型。
所有这些信息确实应该在语言本身中以适当的语法编码。
例如,可以在省略号后使用形式参数扩展现有的 C 语法,如下所示
void foo ( ... int counter, float arglist );
按照惯例,第一个参数可以用于参数计数,第二个参数用于参数列表。在函数体中,列表可以在语法上被视为一个数组。
有了这样的约定,可变参数将不再是匿名的。在函数体内,可以像引用任何其他参数一样引用计数器,并且可以像引用数组参数的数组元素一样引用列表元素,就像这样
void foo ( ... int counter, float arglist ) {
unsigned i;
for (i=0; i<counter; i++) {
printf("list[%i] = %f\n", i, arglist[i]);
}
}
借助语言本身内置的这种功能,每个对的引用都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);
实际参数的类型将在编译时对照相应的可变参数形式参数进行检查。
在函数体内,计数器将再次被其标识符引用,键和值将被引用,就好像它们是数组一样,
key[i]
指第 i 个键/值对的键
value[i]
指第 i 个值对的值
这些引用将被编译到它们各自在堆栈帧上的地址。
这些都不是真的很难做到,也从来没有。然而,C 的设计理念根本不利于这些特性。
如果没有一个冒险的 C 编译器实现者(或 C 预处理器实现者)带头实现这个或类似的方案,我们不太可能在 C 中看到任何这种类型的东西。
问题是那些对类型安全感兴趣并愿意投入工作来构建自己的编译器的人通常会得出这样的结论,即 C 语言已经无法挽救,人们不妨从设计更好的语言开始.
我自己也去过那里,最终决定放弃尝试,然后实现 Wirth 的一种语言,并在其中添加了类型安全的可变参数。从那以后,我遇到了其他人,他们告诉我他们自己失败的尝试。C 中正确的类型安全可变参数似乎仍然难以捉摸。