对于某些函数来说,很难明确指定调用时期望的参数数量和类型。要实现这样的接口我们有三种选择:
- 使用可变模板(见28.6节):它允许我们以类型安全的方式处理任意类型、任意数量的参数,只要写一个小的模板元程序来解释参数列表的正确含义并采取适当的操作就可以了
- 使用 initializer list(见12.2.3节)作为参数类型。它允许我们以类型安全的方式处理某种类型的、任意数量的参数,在大多数上下文中,这种元素类型相同的参数列表是最常见和最重要的情形。
- 用省略号(…)结束参数列表,表示“可能有更多参数”。它允许我们通过使用
中的宏处理(几乎)任意类型的、任意数量的参数。这种方案并非类型安全的,并且很难用于复杂的用户自定义类型。但是,从早期的C语言开始人们就使用这种机制了。
前两种机制将在其他章节中介绍,因此在这里我只为读者描述第三种机制(尽管在大多数情况下,它相对于前两种机制来说是次优选择)。例如
int printf(const char* ...);
这条语句规定对标准库函数prnt(见433节)的调用必须至少有一个C风格字符串的参数,同时可以有也可以没有其他参数。例如:
printf("Hello,world");printf("My name is %s %s\n",first_name,second_name);printf("%d + d%=%d\n",2,3,5);
这样的函数在解释其参数列表时必须依赖于某些编译器不可知的信息。以pintf()为例,它的第一个参数是一条包含特殊字符序列的格式化字符串,该字符串为 printf正确处理其他参数提供了保障。%s表示“期望一个char·参数”,%d表示“期望一个int参数”。然而通常情况下,编译器并不能保证在某次调用中期望的参数一定出现,也不能保证出现的参数定是期望的类型。例如
#include<cstdio>int main(){std::printf("My name is %s %s\n",2);}
这是一段错误的代码,但是大多数编译器都无法发现此类错误。读者不妨试试这段代码,它没什么实际作用,顶多会产生一些奇奇怪怪的输出。
显然,如果某个参数并没有被声明,编译器也就不知道该怎么对它执行标准类型检查和类型转换。此时,char和 short被当成int传递,foat被当成 double传递。程序员并不一定乐于见到这种情况。
在一个设计良好的程序中,不应该出现太多参数类型不确定的函数。当程序员想使用未限定类型的参数时,其实应该优先考虑使用重载函数、带默认参数的函数、接受 initializerlist参数的函数或者可变参数模板,它们不但可以覆盖大多数情况而且能较好地执行类型检査。只有当参数数量和参数类型都不确定且可变模板也不适用时,才考虑使用省略号参数。
省略号最常见的用法是为C语言库函数提供接口:
int fprintf(FILE*,const char* ...); //来自于<cstdio>int execl(const char* ...); //来自于Unix头文件
字符串参数列表以空指针结束:
extern void error(int ...);extern char* (int,char[]); //int转换为字符串int main(int argc,char* argv[]){switch(argc){case 1:error(0,argv[0],nullptr);break;case 2:error(0,argv[0],argv[1],nullptr);break;default:char buffer[8];error(1,argv[0],"with",itoa(argc-1,buffer),"arguments",nullptr);}//...}
函数itoa()返回一个C风格字符串,是其int参数的字符串表示。这种用法在C语言中很流行,但是并不属于C标准的一部分。
我之所以总是传递argv[0]是因为按照惯例它代表程序的名字。
请注意,把整数0作为终止符的做法是不可移植的:在有的实现中,整数0和空指针的表示形式并不一致(见6.2.8节)。这从一个侧面证明了如果用省略号抑制了类型检查,则程序员将不得不额外面对很多微妙的工作。
函数error()的定义如下所示
void error(int severity,initializer_list<string> err){for(auto& s:err)cerr<<s<<'';cerr<<'\n';if(severity) exit(severity);}
要想调用它,必须使用列表记法。例如:
switch(argc){case 1:error(0,argv[0],nullptr);break;case 2:error(0,{argv[0],argv[1]});break;default:error(1,{argv[0],"with",itoa(argc-1,buffer),"arguments"});}
标准库负责提供int向 string的转换函数to_string()(见36.3.5节)。
如果不拘泥于C语言的风格,我们可以给函数传人一个容器,这可以进一步简化代码:
void error(int severity,const vector<string>& err)//与之前几乎一样{for(auto& s:err)cerr<<s<<'';cerr<<'\n';if(severity) exit(severity);}vector<string> arguments(int argc,char* argv[])//把参数打包在一起{vector<string> res;for(int i=0;i!argc;++i)res.push_back(argv[i]);return res;}int main(int argc,char* argv[]){auto args=arguments(argc,argv);error((args.size()<2)?0:1,args);//...}
辅助函数arguments()并不复杂,同时main()和error()也很简单。main()和error()之间的接口传递了所有的参数,因此通用性更强且使得我们可以进一步改进 error()。此外,与未指定数量的参数相比,使用 vector< string>出错的可能性大大降低了。
