对于函数参数而言,C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为:
int printf ( const char * format, ... );
...
就表示的是可变参数,即 printf() 函数可以接收任意个参数,且各个参数的类型可以不同,例如:
printf("%d", 10); printf("%d %c",10, 'A'); printf("%d %c %f",10, 'A', 1.23);
我们通常将容纳多个参数的可变参数称为参数包。借助 format 字符串,printf() 函数可以轻松判断出参数包中的参数个数和类型。
下面的程序中,自定义了一个简单的可变参数函数:
#include <iostream> #include <cstdarg> //可变参数的函数 void vair_fun(int count, ...) { va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { int arg = va_arg(args, int); std::cout << arg << " "; } va_end(args); } int main() { //可变参数有 4 个,分别为 10、20、30、40 vair_fun(4, 10, 20, 30,40); return 0; }
程序中的 vair_fun() 函数有 2 个参数,一个是 count,另一个就是 … 可变参数。我们可以很容易在函数内部使用 count 参数,但要想使用参数包中的参数,需要借助<cstdarg>
头文件中的 va_start、va_arg 以及 va_end 这 3 个带参数的宏:
- va_start(args, count):args 是 va_list 类型的变量,我们可以简单的将其视为 char * 类型。借助 count 参数,找到可变参数的起始位置并赋值给 args;
- va_arg(args, int):调用 va_start 找到可变参数起始位置的前提下,通过指明参数类型为 int,va_arg 就可以将可变参数中的第一个参数返回;
- va_end(args):不再使用 args 变量后,应及时调用 va_end 宏清理 args 变量。
注意,借助 va_arg 获取参数包中的参数时,va_arg 不具备自行终止的能力,所以程序中借助 count 参数控制 va_arg 的执行次数,继而将所有的参数读取出来。控制 va_arg 执行次数还有其他方法,比如读取到指定数据时终止。
使用 … 可变参数的过程中,需注意以下几点:
- … 可变参数必须作为函数的最后一个参数,且一个函数最多只能拥有 1 个可变参数。
- 可变参数的前面至少要有 1 个有名参数(例如上面例子中的 count 参数);
- 当可变参数中包含 char 类型的参数时,va_arg 宏要以 int 类型的方式读取;当可变参数中包含 short 类型的参数时,va_arg 宏要以 double 类型的方式读取。
需要注意的是,… 可变参数的方法仅适用于函数参数,并不适用于模板参数。C++11 标准中,提供了一种实现可变模板参数的方法。
可变参数模板
C++ 11 标准发布之前,函数模板和类模板只能设定固定数量的模板参数。C++11 标准对模板的功能进行了扩展,允许模板中包含任意数量的模板参数,这样的模板又称可变参数模板。
1) 可变参数函数模板
先讲解函数模板,如下定义了一个可变参数的函数模板:
template<typename... T> void vair_fun(T...args) { //函数体 }
模板参数中, typename(或者 class)后跟 … 就表明 T 是一个可变模板参数,它可以接收多种数据类型,又称模板参数包。vair_fun() 函数中,args 参数的类型用 T… 表示,表示 args 参数可以接收任意个参数,又称函数参数包。
这也就意味着,此函数模板最终实例化出的 vair_fun() 函数可以指定任意类型、任意数量的参数。例如,我们可以这样使用这个函数模板:
vair_fun(); vair_fun(1, "abc"); vair_fun(1, "abc", 1.23);
使用可变参数模板的难点在于,如何在模板函数内部“解开”参数包(使用包内的数据),这里给大家介绍两种简单的方法。
【递归方式解包】
先看一个实例:
#include <iostream> using namespace std; //模板函数递归的出口 void vir_fun() { } template <typename T, typename... args> void vir_fun(T argc, args... argv) { cout << argc << endl; //开始递归,将第一个参数外的 argv 参数包重新传递给 vir_fun vir_fun(argv...); } int main() { vir_fun(1, "http://www.biancheng.net", 2.34); return 0; }
执行结果为:
1
http://www.biancheng.net
2.34
分析一个程序的执行流程:
- 首先,main() 函数调用 vir_fun() 模板函数时,根据所传实参的值,可以很轻易地判断出模板参数 T 的类型为 int,函数参数 argc 的值为 1,剩余的模板参数和函数参数都分别位于 args 和 argv 中;
- vir_fun() 函数中,首先输出了 argc 的值(为 1),然后重复调用自身,同时将函数参数包 argv 中的数据作为实参传递给形参 argc 和 argv;
- 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 char*,输出 argc 的值为 "http:www.biancheng.net"。再次调用自身,继续将 argv 包中的数据作为实参;
- 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 double,输出 argc 的值为 2.34。再次调用自身,将空的 argv 包作为实参;
- 由于 argv 包没有数据,此时会调用无任何形参、函数体为空的 vir_fun() 函数,最终执行结束。
以递归方式解包,一定要设置递归结束的出口。例如本例中,无形参、函数体为空的 vir_fun() 函数就是递归结束的出口。
【非递归方法解包】
借助逗号表达式和初始化列表,也可以解开参数包。
以 vir_fun() 函数为例,下面程序演示了非递归方法解包的过程:
#include <iostream> using namespace std; template <typename T> void dispaly(T t) { cout << t << endl; } template <typename... args> void vir_fun(args... argv) { //逗号表达式+初始化列表 int arr[] = { (dispaly(argv),0)... }; } int main() { vir_fun(1, "http://www.biancheng.net", 2.34); return 0; }
这里重点分析一下第 13 行代码,我们以{ }
初始化列表的方式对数组 arr 进行了初始化, (display(argv),0)… 会依次展开为 (display(1),0)、(display("http://www.biancheng.net"),0) 和 (display(2.34),0)。也就是说,第 13 行代码和如下代码是等价的:
int arr[] = { (dispaly(1),0), (dispaly("http://www.biancheng.net"),0), (dispaly(2.34),0) };
可以看到,每个元素都是一个逗号表达式,以 (display(1), 0) 为例,它会先计算 display(1),然后将 0 作为整个表达式的值返回给数组,因此 arr 数组最终存储的都是 0。arr 数组纯粹是为了将参数包展开,没有发挥其它作用。
2) 可变参数类模板
C++11 标准中,类模板中的模板参数也可以是一个可变参数。C++ 11 标准提供的 typle 元组类就是一个典型的可变参数模板类,它的定义如下:
template <typename... Types> class tuple;
和固定模板参数的类不同,typle 模板类实例化时,可以接收任意数量、任意类型的模板参数,例如:
std:tuple<> tp0; std::tuple<int> tp1 = std::make_tuple(1); std::tuple<int, double> tp2 = std::make_tuple(1, 2.34); std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.34, "http://www.biancheng.net");
如下代码展示了一个支持可变参数的类模板:
#include <iostream> //声明模板类demo template<typename... Values> class demo; //继承式递归的出口 template<> class demo<> {}; //以继承的方式解包 template<typename Head, typename... Tail> class demo<Head, Tail...> : private demo<Tail...> { public: demo(Head v, Tail... vtail) : m_head(v), demo<Tail...>(vtail...) { dis_head(); } void dis_head() { std::cout << m_head << std::endl; } protected: Head m_head; }; int main() { demo<int, float, std::string> t(1, 2.34, "http://www.biancheng.net"); return 0; }
程序中,demo 模板参数中的 Tail 就是一个参数包,解包的方式是以“递归+继承”的方式实现的。具体来讲,demo<Head, Tail…> 类实例化时,由于其继承自 demo<Tail…> 类,因此父类也会实例化,一直递归至 Tail 参数包为空,此时会调用模板参数列表为空的 demo 模板类。
程序的输出结果为:
http://www.biancheng.net
2.34
1
可变参数模板类还有其它的解包方法,这里不再一一赘述,感兴趣的读者可以自行做深入的研究。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21435.html