C++11引入可变参数模板,使函数模板能接受任意数量、任意类型的参数。定义时用class...或typename...表示类型包,Args...args表示形参包,并通过sizeof...获取参数个数,极大增强了模板的通用性与灵活性。
这篇专题适合已经学过C++的同学复习使用,重点巩固可变参数模板的相关知识。
可变参数模板是C++11引入的新特性,它使参数的高度泛化成为可能——我们终于可以创建能够接受可变参数的函数模板和类模板了。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
printf函数可以接收任意多个参数。但需要注意,那是函数参数的可变参数,并非模板的可变参数。注:本篇只涉及函数模板的可变参数,类模板的内容不在此讨论。
函数的可变参数模板定义起来比较直观,请看以下格式:
template返回类型 函数名(Args...args) { // 函数体 }
举个例子:
templatevoid ShowList(Args... args) {}
注意这里的省略号:在模板参数列表中,class... 或 typename... 表示后面跟着的是一个类型包,可以匹配0个或多个类型;在函数参数列表中,类型名后面跟 ... 表示后面跟着的是一个形参对象包,同样可以匹配0个或多个对象。此外,函数参数包可以使用左值引用或右值引用表示,每个参数实例化时都会遵循引用折叠规则。
这样一来,调用ShowList函数时可以传入任意多个参数,而且这些参数的类型可以各不相同。例如:
int main()
{
ShowList();
ShowList(1);
ShowList(1,'A');
ShowList(1,'A',string("hello"));
return 0;
}
我们还可以在函数模板中通过sizeof...来获取参数包中参数的个数:
templatevoid ShowList(Args... args) { cout << sizeof...(args) << endl; } int main() { ShowList(); // 0 ShowList(1); // 1 ShowList(1,'A'); // 2 ShowList(1,'A',string("hello")); // 3 return 0; }
但有一个关键问题:我们不能直接通过下标来获取参数包中的每个参数,只能通过展开参数包的方式来处理它们。这也是可变参数模板最大的特点和难点。
其本质实际上有两种理解方式:
1. 结合引用折叠规则,编译器会实例化出以下这些函数:
void ShowList(); void ShowList(int arg1); void ShowList(int arg1,char arg2); void ShowList(int arg1,char arg2,string arg3);
2. 从更本质的角度看,生成的是下面这样的模板:
void ShowList(); templatevoid ShowList(T1 arg1); template void ShowList(T1 arg1,T2 arg2); template void ShowList(T1 arg1, T2 arg2,T3 arg3);
需要特别强调的是:语法并不支持用 args[i] 的方式来获取参数包中的参数。下面的写法是错误的:
templatevoid ShowList(Args... args) { for (int i = 0; i < sizeof...(args); i++) { cout << args[i] << " "; } cout << endl; }
因此,要获取参数包中的各个参数,只能通过展开参数包的方式实现。常用的展开方式有两种:递归展开和逗号表达式展开。
递归展开的核心思路如下:
例如,想要打印调用函数时传入的各个参数,可以这样写:
//展开函数 templatevoid ShowList(T value, Args... args) { cout << value << " "; //打印分离出的第一个参数 ShowList(args...); //递归调用,将参数包继续向下传 }
接下来需要解决的是:递归如何终止?
编写无参的递归终止函数
我们可以再写一个无参的同名函数作为递归终止条件:
//递归终止函数
void ShowList()
{
cout << endl;
}
//展开函数
template
void ShowList(T value, Args... args)
{
cout << value << " "; //打印分离出的第一个参数
ShowList(args...); //递归调用,将参数包继续向下传
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', string("hello"));
return 0;
}
当参数包中的参数个数为0时,递归调用就会匹配到那个无参的终止函数,递归结束。
当然,也可以换一种写法,把ShowList和Print分开来:
void ShowList()
{
cout << endl;
}
template
void ShowList(T x, Args... args)
{
cout << x << " ";
ShowList(args...);
}
// 编译时递归推导解析参数
template
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print(1);
Print(1, 'A');
Print(1, 'A', string("hello"));
return 0;
}
1. 通过列表获取参数包中的参数
如果参数包中所有参数的类型都是整型,可以直接将这个参数包放到一个列表里,用来初始化一个整型数组,这样参数包中的参数就被放入数组中了:
templatevoid Print(Args...args) { int arr[] = { args...}; for (auto e : arr) { cout << e << " "; } cout << endl; } int main() { Print(1); Print(1,2); Print(1,2,3); return 0; }
但C++不像Python这类语言——C++要求容器中存储的数据类型必须一致。因此如果这样写,调用ShowList时传入的参数只能是整型,而且不能传0个参数,因为数组大小不能为0。要解决这个问题,就需要借助逗号表达式。
2. 通过逗号表达式展开参数包
虽然不能用不同类型的参数去初始化整型数组,但逗号表达式可以帮我们实现。
这样,在执行逗号表达式时,会先调用那个处理函数来处理对应的参数,然后用最后一个整型值去初始化整型数组。例如:
templateconst T& GetArg(const T& x) { cout << x << " "; return x; } template void Arguments(Args... args) {} template void Print(Args... args) { Arguments(GetArg(args)...); } int main() { Print(1, string("2345"), 1.1); return 0; }
其本质是:编译器在编译时会实例化出以下函数:
templateconst T& GetArg(const T& x) { cout << x << " "; return x; } template void Arguments(Args... args) {} template void Print(int x,string y,double z) { Arguments(GetArg(x),GetArg(y),GetArg(z)); } int main() { Print(1, string("2345"), 1.1); return 0; }
C++11标准为STL中的容器新增了emplace版本的插入接口。例如list容器的push_front、push_back和insert,都增加了对应的emplace_front、emplace_back和emplace函数。如下所示:


注意: emplace系列接口的可变模板参数类型都带有“&&”,这表示万能引用,而非右值引用。
emplace系列接口的使用方式
emplace系列接口的使用方式与容器原有的插入接口有些类似,但也存在不同之处。
以list容器的emplace_back和push_back为例:
push_back插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。emplace_back插入元素时,同样可以传入左值对象或者右值对象,但不能使用列表进行初始化。emplace系列接口的意义
由于emplace系列接口的可变模板参数类型都是万能引用,所以既可以接收左值对象,也可以接收右值对象,还能接收参数包。
总结如下:
当然,这里有一个前提:容器中存储的元素对应的类必须是需要深拷贝的类,并且该类实现了移动构造函数。否则,调用emplace系列接口时,传入左值对象和传入右值对象的效果相同——都需要一次构造函数加一次拷贝构造函数。
实际上,emplace系列接口的一部分功能与容器原有的插入接口是重叠的。因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口。如果调用这些接口时传入的是右值对象,最终也会调用对应的移动构造函数来移动资源。
emplace接口的意义:
验证
要验证上述说法,需要一个深拷贝的类。下面模拟实现了一个简化版的string类,只包含我们需要的成员函数:
namespace xxhh
{
class string
{
public:
//构造函数
string(const char* str = "")
{
cout << "string(const char* str) -- 构造函数" << endl;
_size = strlen(str); //初始时,字符串大小设置为字符串长度
_capacity = _size; //初始时,字符串容量设置为字符串长度
_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
strcpy(_str, str); //将C字符串拷贝到已开好的空间
}
//交换两个对象的数据
void swap(string& s)
{
//调用库里的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
//拷贝构造函数(现代写法)
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
swap(tmp); //交换这两个对象
}
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
//拷贝赋值函数(现代写法)
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s); //用s拷贝构造出对象tmp
swap(tmp); //交换这两个对象
return *this; //返回左值(支持连续赋值)
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
//析构函数
~string()
{
//delete[] _str; //释放_str指向的空间
_str = nullptr; //及时置空,防止非法访问
_size = 0; //大小置0
_capacity = 0; //容量置0
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
由于我们在string的构造函数、拷贝构造函数和移动构造函数中都打印了提示语句,因此可以通过控制台输出来判断这些函数是否被调用。
下面用一个容器来存储这个模拟的string,并以不同的传参形式调用emplace系列函数:
#includeint main() { list
> mylist; pair kv(1, "one"); mylist.emplace_back(kv); //传左值 cout << endl; mylist.emplace_back(pair (2, "two")); //传右值 cout << endl; mylist.emplace_back(3, "three"); //传参数包 return 0; }

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述