1.C 实现变参函数

C 语言中,有时需要变参函数来完成特殊的功能,比如 C 标准库函数 printf() 和 scanf()。C 中提供了省略符...完成变参函数的书写。

变参函数原型申明如下:

type functionname(type param1, ...);

变参函数至少要有一个固定参数,省略号“…”不可省略,比如 printf() 原型如下:

int printf(const char *format,...);

在头文件 stdarg.h 中定义了三个宏函数用于获取指定类型的实参:

void	va_start(va_list arg,prev_param);    
type    va_arg(va_list arg,type);
void    va_end(va_list arg);

va 在这里是 可变参数(variable argument)的意思,借助上面三个宏函数,变参函数的实现就变得相对简单很多。一般的变参函数处理过程:
(1)定义一个 va_list 变量设为 va;
(2)调用 va_start() 使得va存放变参函数的变参前的一个固定参数的地址;
(3)不断调用 va_arg() 使得va指向下一个实参;
(4)最后调用 va_end() 表示变参处理完成,将 va 置空。
原理就是:函数的参数在内存中从低地址向高地址依次存放。

看一个例子:模仿 pritnf() 的实现:

#include<iostream>  
#include<stdarg.h>  
#include<string.h>  
using namespace std;  
  
void func(char *c,...) {        
    int i=0;  
    double result=0;  
	va_list arg;        //va_list变量  
    va_start(arg,c);    //arg指向固定参数c  
    while(c[i]!='\0') {      
        if(c[i]=='%'&&c[i+1]=='d') {  
            printf("%d",va_arg(arg,int));  
            i++;  
        } else if(c[i]=='%'&&c[i+1]=='f') {  
            printf("%f",va_arg(arg,double));  
            i++;  
        } else {
            putchar(c[i]);
        } 
        i++;  
    }  
    va_end(arg);  
}  
  
int main() {  
    int i=100;  
    double j=100.0;  
    printf("%d be equal %f\n",i,j);  
    func("%d be equal %f\n",i,j);
	system("pause");
}

程序输出:

100 be equal 100.000000
100 be equal 100.000000

C 变参函数缺点 [ 2 ] ^{[2]} [2]
(1)缺乏类型检查,容易出现不合理的强制类型转换。在获取实参时,是通过给定的类型进行获取,如果给定的类型与实际参数类型不符,则会出现类型安全性问题,容易导致获取实参失败。
(2)不支持自定义类型。自定义类型在程序中经常用到,比如我们要使用printf()来打印一个Student类型的对象的内容,该用什么格式字符串去指定实参类型,通过C提供的va_list,我们无法提取实参内容。

鉴于以上两点,李健老师在其著作《编写高质量代码改善C++程序的150个建议》建议尽量不要使用 C 风格的变参函数。

2.C++ 实现变参函数

为了编写能够处理不同数量实参的函数,C++11提供了两种主要方法:
(1)如果所有实参类型相同,可以传递标准库类型initializer_list;
(2)如果实参类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。

2.1 initializer_list 形参

initializer_list 是 C++11 引入的一种标准库类模板,用于表示某种特定类型值的数组。initializer_list类型定义在同名的头文件中,它提供的操作有:

initializer_list<T> lst;			//默认初始化T类型的空列表。
initializer_list<T> lst{a,b,c,...};	//lst的元素是对应初始值的副本,且列表中的元素是const。
lst2(lst);		//拷贝构造一个initializer_list对象,不拷贝列表中的元素,与原始列表共享元素
lst2=lst;		//赋值,与原始列表共享元素。

lst.size();		//列表中的元素数量。
lst.begin();	//返回指向lst中首元素的指针。
lst.end();		//返回lst中尾元素下一位置的指针。

和 vector 与 list 一样,initializer_list 也是一种模板类型。定义 initializer_list 对象时必须指明列表中所含元素的类型。与 vector 和 list 不同之处在于 initializer_list 中的元素不可修改,并且拷贝构造和赋值时元素不会被拷贝。如此设计,让 initializer_list 更加符合参数通过指针传递,而非值传递,提高性能。所以 C++11 采用了 initializer_list 作为变参函数的形参。

下面给出一个打印错误的变参函数:

void error_msg(initializer_list<string> il) {
	for(auto beg=il.begin();beg!=il.end()) {
		cout<<*beg<<" ";
	}
	cout<<endl;
}

2.2 可变参数模板

简介:
目前大部分主流编译器的最新版本均支持了C++11标准(官方名为ISO/IEC14882:2011)大部分的语法特性,其中比较难理解的新语法特性可能要属可变参数模板(variadic template)了,GCC 4.6和Visual studio 2013都已经支持变参模板。可变参数模板就是一个接受可变数目参数的函数模板或类模板。可变数目的参数被称为参数包(parameter packet),这个也是新引入C++的概念,可以细分为两种参数包:
(1)模板参数包(template parameter packet),表示零个或多个模板参数。
(2)函数参数包(function parameter packet),表示零个或多个函数参数。

可变参数模板示例:
使用省略号…来指明一个模板的参数包,在模板参数列表中,class...typename...指出接下来的参数表示零个或多个类型参数;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数。声明一个带有可变参数个数的模板的语法如下所示:

// 1.申明可变参数的类模板
template<typename... Types> class tuple;
tuple<int, string> a;  // use it like this

// 2.申明可变参数的函数模板
template<typename T,typename... Types> void foo(const T& t,const Types&... rest);
foo<int,float,double,string>(1,2.0,3.0,"lvlv");//use like this

// 3.申明可变非类型参数的函数模板(可变非类型参数也可用于类模板)
template<typename T,unsigned... args> void foo(const T& t);
foo<string,1,2>("lvlv");//use like this

其中第一条示例中Types就是模板参数包,第二条示例中rest就是函数参数包,第三条示例中args就是非类型模板参数包。

参数包扩展:
现在我们知道parameter packet了,怎么在程序中真正具体地去处理打包进来的“任意个数”的参数呢?也就是说可变参数模板,我们如何进行参数包的扩展,获取传入的参数包中的每一个实参呢?对于一个参数包,可以通过运算符sizeof…来获取参数包中的参数个数,比如:

template<typename... Types> void g(Types... args) {
	cout<<sizeof...(Types)<<endl;  //类型参数数目
	cout<<sizeof...(args)<<endl;   //函数参数数目
}

我们能够对参数包唯一能做的事情就是对其进行扩展,扩展一个包就是将它分解为构成的元素,通过在参数包的右边放置一个省略号…来触发扩展操作,例如:

template<typename T,typename... Types> ostream& print(ostream& os,const T& t,const Types&... rest) {
	os<<t<<",";
	return print(os,rest...);
}

上面的示例代码中,存在两种包扩展操作:
(1)const Types&... rest表示模板参数包的扩展,为print函数生成形参列表;
(2)对print的调用中rest...表示函数参数包的扩展,为print调用生成实参列表。

可变参数函数实例:
可变参数函数通常以递归的方式来获取参数包的每一个参数。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。最后,定义一个非可变参数的同名函数模板来终止递归。我们以自定义的print函数为例,实现如下:

#include <iostream>  
using namespace std;

template<typename T> ostream& print(ostream& os,const T& t){
	os<<t<<endl;  //包中最后一个元素之后打印换行符
}

template<typename T,typename... Types> ostream& print(ostream& os,const T& t,const Types&... rest){
	os<<t<<",";  //打印第一个实参
	print(os,rest...);  //递归调用,打印其他实参
}

int main(){
	print(cout,10,123.0,"lvlv",1); //例1
	print(cout,1,"lvlv0","lvlv1");
}

程序输出:

10,123,lvlv,1
1,lvlv0,lvlv1

上面递归调用print,以例1为例,执行的过程如下:

调用trest…
print(cout,10,123.0,“lvlv”,1)10123.0,“lvlv”,1
print(cout,123.0,“lvlv”,1)123.0“lvlv”,1
print(cout,“lvlv”,1)“lvlv”1
print(cout,1),调用非变参版本的print1
前三个调用只能与可变参数版本的print匹配,非变参版本是不可行的,因为这三个调用要传递两个以上实参,非可变参数的print只接受两个实参。对于最后一次递归调用print(cout,1),两个版本的print都可以,因为这个调用传递两个实参,第一个实参的类型为ostream&,另一个是const T&参数。但是由于非可变参数模板比可变参数模板更加特例化,因此编译器选择非可变参数版本。

参考文献

编写高质量代码改善C++程序的150个建议.李健.2012.P34-35
Stanley B. Lippman著,王刚 杨巨峰译.C++ Primer中文版第五版.2013.P197-199
c /c++变参函数

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐