6.2 变长模板
类别:库作者
6.2.1 变长函数和变长的模板参数
在2.1节中,我们知道C++11已经支持了C99的变长宏。变长宏与printf的默契配合使得程序员能够非常容易地派生出printf的变种以支持一些记录。而如我们提到的,printf则使用了C语言的函数变长参数特性,通过使用变长函数(variadic funciton),printf的实现能够接受任何长度的参数列表。不过无论是宏,还是变长参数,整个机制的设计上,没有任何一个对于传递参数的类型是了解的。我们可以看看变长函数的例子。通常情况下,一个变长函数可以如代码清单6-9所示。
代码清单6-9
include <stdio.h>
include <stdarg.h>
double SumOfFloat(int count,…){
va_list ap;
double sum=0;
va_start(ap,count);//获得变长列表的句柄ap
for(int i=0;i<count;i++)
sum+=va_arg(ap,double);//每次获得一个参数
va_end(ap);
return sum;
}
int main(){
printf("%f\n",SumOfFloat(3,1.2f,3.4,5.6));//10.200000
}
//编译选项:gcc 6-2-1.cpp
在代码清单6-9中,我们声明了一个名为SumOfFloat变长函数。变长函数的第一个参数count表示的是变长参数的个数,这必须由SumOfFloat的调用者传递进来。而在被调用者中,则需要通过一个类型为va_list的数据结构ap来辅助地获得参数。可以看到,这里代码首先使用va_start函数对ap进行初始化,使得ap成为被传递的变长参数的一个“句柄”(handler)。而后代码再使用va_arg函数从ap中将参数一一取出用于运算。由于这里是计算浮点数的和,所以每次总是给va_arg传递一个double类型作为参数。图6-1显示了一种变长函数的可能的实现方式,即以句柄ap为指向各个变长参数的指针,而va_arg则通过改变指针的方式(每次增加sizeof(double)字节)来返回下一个指针所指向的对象。
图 6-1 变长函数可能的实现方式
可以看到,在本例中,只有使用表达式va_arg(ap,double)的时候,我们才按照类型(实际是按类型长度)去变长参数列表中获得指定参数。而如何打印则得益于传递在字符串中的形如“%s”、“%d”这样的转义字,以及传递的count参数。事实上,函数“本身”完全无法知道参数数量或者参数类型。因此,对于一些没有定义转义字的非POD的数据来说,使用变长函数就会导致未定义的程序行为。比如:
const char*msg="hello%s";
printf(msg,std::string("world"));
这样的代码就会导致printf出错。
从另一个角度讲,变长函数这种实现方式,对于C++这种强调类型的语言来说相当于开了一个“不规范”的后门。这是C++标准中所不愿意看到的(即使它能够工作)。因此,客观上,C++需要引入一种更为“现代化”的变长参数的实现方式,即类型和变量同时能够传递给变长参数的函数。一个好的方式就是使用C++的函数模板。在C++98中,标准要求函数模板始终具有数目确定的模板参数及函数参数。因此如果要实现一个新的变长参数的函数的话,我们需要突破参数的限制。
此外在一些情况下,类也需要不定长度的模板参数。最为典型的就是C++11标准库中的tuple类模板。如果读者熟悉C++98中的pair类模板的话,那么理解tuple也就不困难了。具体来讲,pair是两个不同类型的数据的集合。比如pair<int,double>就能够容纳int类型和double类型的两种数据。一些如std::map的标准库容器,其成员就需要是类模板pair的。在C++11中,tuple是pair类的一种更为泛化的表现形式。比起pair,tuple是可以接受任意多个不同类型的元素的集合。比如我们可以通过:
std::tuple<double,char,std::string>collections;
来声明一个tuple模板类。该collections变量可以容纳double、char、std::string三种类型的数据。当然,读者还可以用更多的参数来声明collection,因为tuple可以接受任意多的参数。此外,和pair类似地,我们也可以更为简单地使用C++11的模板函数make_tuple来创造一个tuple模板类型。
std::make_tuple(9.8,'g',"gravity");
由于tuple包含的类型数量可以任意地多,那么在客观上,就需要类模板能够接受变长的参数。因此,在C++11中我们就看到了所谓的变长模板(variadic template)的实现。
注意 在C++98中,由于没有变长模板,tuple能够支持的模板参数数量实际上是有限的。这个数量是由标准库定义了多少个不同参数版本的tuple模板而决定的。