什么是可变形参函数
在c++编程中,有时我们需要编写一些在源代码编写阶段无法确定参数个数,有时甚至无法确定参数类型的函数。
例如,一个求和函数。可以通过重载实现若干个数的和。
int sum(int i1, int i2);
int sum(int i1, int i2, int i3);
...//还可以重载更多类似函数
double sum(double d1, double d2);
double sum(double d1, double d2, double d3);
...//还可以重载更多类似函数
- 1
- 2
- 3
- 4
- 5
- 6
- 7
以上代码通过重载机制来解决变参问题。但很快我们就会发现这种方法存在的问题:必须确保所有可能的实参列表都有对应的重载声明和定义,如果上述方法如果参与运算的参数个数可能从2——20个不等,那么我们就需要重载19次同一个函数。
我们需要的是这样一类函数:它们可以在运行时取任意的实参个数并根据实参的个数自动处理不同实参的情形,或者至少可以在运行时指定任意的实参个数。
实现变参函数的三种方法
在C++中实现一个变参函数的方法有三种:第一种方法,将函数形参声明为C++11新标准中的initializer_list标准库类型;第二种方法继承自C语言,形参声明为省略符,函数实现时用参数列表宏访问参数;最后一种方法利用C++泛型特性,声明一个可变参数模板来实现。
1. 可变参数宏
实现步骤如下:
1. 函数原型中使用省略号;
2. 函数定义中创建一个va_list变量;
3. 初始化va_list变量;
4. 访问参数列表;
5. 完成清理工作;
上述步骤的实现需要使用到四个宏:
va_list、va_start(va_list, arg)、va_arg(va_list, type)、va_end(va_list)
这些宏在
头文件stdarg.h
中声明定义。因此使用时需要包含该头文件。
以下代码使用可变参数宏实现一个函数sum,该函数接受任意个数的整形实参,返回这些实参的和。(忽略可能存在的整形溢出)
/* --sum.cpp-- 可变参数宏实现求任意个整形值得和 */
#include <stdarg.h>
int sum(int count, ...); //原型中使用省略号
int sum(int count, ...){ //count 表示可变参数个数
va_list ap; //声明一个va_list变量
va_start(ap, count); //初始化,第二个参数为最后一个确定的形参
int sum = 0;
for(int i = 0; i < count; i++)
sum += va_arg(ap, int); //读取可变参数,的二个参数为可变参数的类型
va_end(ap); //清理工作
return sum;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
使用这种方法需要注意一下几点:
1. 函数原型中,
省略号必须在参数列表的末尾
:也就是说,在函数原型中参数列表省略号的右边不能再出现确定参数;
2. 运行时,函数必须能够根据已有信息(既有约定,或确定实参)
确定可变参数的具体个数与类型
:函数定义需要知道可变参数的具体类型、个数,这些信息是在运行时确定的,那么显然应该由实参来确定。在上面的例子中count传递了可变参数的个数,而参数类型则是既有约定(整形);
3. 使用完成时需要
用va_end()做清理工作
,可变参数宏可能使用了动态分配的内存,忘记执行清理操作有可能导致内存泄漏等问题;
4. 可变参数宏只能实现顺序访问可变参数,无法后退访问,但是可以在清理操作完成后重新使用va_start初始化va_list变量,重新遍历形参表;
5. 该方法是
极不安全
的,宏本身无法提供任何安全性保证,他总是按照既定代码“自作多情”的认为实参就应该是那么多,即使实参并不是那么多。这就要求所有安全性必须由程序员来保证。例如,在以上的示例代码中,如果调用时指定count为10,但实际上只给出9个可变形参,那么函数还是会读取10个参数,显然第十次读取是多余的,多余的操作一般不会有什么好结果,当然如果实参过多,多余的实参也不会被读取而是被忽略。
使用这种方法的一个实例是printf()函数。printf()函数通过分析第一个字符串参数中的占位符个数来确定形参的个数;通过占位符的不同来确定参数类型(%d表示int类型、%s表示char *);它也有上述提到的安全问题,如果不小心少提供了个实参,那么越界访问就会发生。
2. initializer_list标准库类型
实现步骤如下:
1. 函数原型中使用实例化
initializer_list
模板代表可变参数列表;
2. 使用迭代器访问initializer_list中的参数;
3. 传入实参写在{}之内。
以上步骤中使用到initializer_list。这是C++11新标准中引入的一个标准库类型,与vector等容器一样initializer_list也支持
begin()和end()
操作,返回指向首元素的迭代器和尾后迭代器。initializer_list在同名头文件中声明,其实现由编译器支持。
以下代码使用initializer_list实现函数sum。(忽略可能存在的整形溢出)
/* --sum.cpp-- 利用initializer_list模板实现求人一个整形值得和 */
#include <initializer_list>
int sum(initializer_list<int> il); //函数原型用int实例化initializer_list作为形参
int sum(inttializer_list<int> il){
int sum = 0;
for(auto p = il.begin(); p != il.end(); p++) //使用迭代器访问参数
sum += *p;
return sum;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
使用这种方法需要注意一下几点:
1. initializer_list在C++11中才被引入,这意味着在编译时可能需要加上这个选项
-std=c++11
才能成功编译。上述代码中的auto关键字也是C++11的一部分;
2. 参数必须
放在一组‘{}’(大括号)内
,编译器通过大括号来将这组参数转化为initializer_list.大括号的的一组实参与initializer_list形参对应;
3. 函数原型initializer_list与普通形参无异。这表明形参列表中可以包含其他类型参数且位置不限,以下函数原型是正确的:
void func(char c, initializer_list<int> il, double d);
- 1
4. 同一个initializer_list中的参数具有相同的类型。本质上来说initializer_list是一个编译器支持的容器类模板,同其他容器一样,容器中的
元素具有相同的类型
。
使用这种方法的一个实例是C++11中vector的列表初始化构造函数。
3. 可变参数模板
在介绍这种方法之前需要先介绍两个并不常用的概念:模板参数包和函数参数包。
模板参数包
是零个或多个类型参数的集合。模板参数列表中,class…或typename…表明其后的类型参数表示一个模板参数包;
函数参数包
是零个或多个非类型参数的集合。函数形参列表中类型名加省略号表明其后的参数表示一个函数参数包;另外,类型为模板参数包的函数形参是一个函数参数包。
以下引用参考书目2中的示例代码来直观展现这两个概念:
//args是一个模板参数包;rest是一个函数参数包
//args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
template<typename T, typename... args>
void foo(const T &t, const args&... rest);
- 1
- 2
- 3
- 4
- 5
与sizeof()运算符类似,sizeof…()运算符用于参数包。
sizeof…()将返回参数包中参数个数
。
利用可变参数模板实现可变参数函数的步骤如下:
1. 编写含有模板参数包和函数参数包的模板函数;
2. 函数定义
递归
调用自己,每一步递归参数包中参数减一;
3. 编写处理
边界情况
(参数包含有零个参数)的模板。
以下引用参考书目2中示例代码:
//用来终止递归并答应最后一个元素的函数
//此函数必须在可变参数版本的print定义之前声明
template <typename T>
std::ostream &print(std::ostream &os, const T &t){
return os << t; //包中最后一个元素
}
//包中除最后一个元素之外的其他元素都会调用这个版本的pirnt
template <typename T, typename... Args>
std::ostream &print(std::ostream &os, const T &t, cosnt Args &... rest){
os << t << ","; //打印第一个实参,包中元素减一
return print(os, rest...); //递归调用,打印剩余实参
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
使用这种方法需要注意的是:
1. 必须处理边界情况。且如代码注释所示:应当首先定义处理边界情况的模板。
2. 参数包在参数列表最右侧,参数包只能从左至右展开?
3. 参数包能够实现更加复杂的模板,更多内容参考C++ Primer(第五版)第16章相关内容。
这种实现方式的根本原理实际上与最初提到的重载是一致的。通过定义模板,让编译器根据实参类型自动生成对应的重载函数。
三种实现方法的比较
以上提到的三种方法都可以实现变参函数。但三种方法都有其各自的有点和局限性,在选择时可以从以下几个方面考虑:
1.
若非必要,不要使用可变参数函数
。应该首先考虑函数重载等其他方法。
2.
除非需要兼容C语言编译器,否则不要使用可变参数宏
。应为这种方法最不安全;尤其是当参数为对象时这种方法易产生各种问题。毕竟这些宏是为C语言设计的,C语言中没有对象。
3. 如果参数类型相同且C++11可用,则通过声明形参为
initializer_list往往是最简单、最有效的办法
。
4. 变参模板看似最为强大。参数的类型可以不同、比可变参数宏更加安全并且可以自动推断参数类型和参数个数。但考虑到模板会为每一个不同的实例生成代码,如果函数的实例过多可能会
使代码体积增大
。另外,依靠
递归
使得功能
具有局限性
,并且
效率
也会受到影响。
【参考书目】
1. Stephen Prata 著,云巅工作室 译,C Primer Plus(第五版)中文版 P478-480, 人民邮电出版社;
2. Stanley B.Lippman, Josee Lajoie, Barbara E.Moo 著, 王刚 杨巨峰 译, C++ Primer(第五版)中文版, 电子工业出版社。