其实可变参数是我们的老熟人了,我们平时使用的printf、scanf,它们的函数参数就是可变参数。


C++11看中了可变参数的优势,随之可变参数模板应运而生……
C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}注意:
个模版参数。
现在,我们可以传入任意数量和任意类型的参数到ShowList中了
int main()
{
ShowList();
ShowList(1);
ShowList(1, 1.1);
ShowList(1, 1.1, 'a');
ShowList(1, 1.1, 'a', string("woxiangchiyu"));
return 0;
}sizeof…运算符计算参数包中参数的个数
使用方式: sizeof...(参数包)
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
return 0;
}我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点。
具体逻辑:
template <class T, class ...Args>
void _ShowList(T val, Args... args)
{
cout << val << " ";//打印分离出的第一个参数
_ShowList(args...);//递归调用自身函数,调用时只传参数包
}// 结束条件的函数
void _ShowList()
{
cout << endl;
}template <class ...Args>
void CppPrint(Args... args)
{
_ShowList(args...);
}递归函数代码如下:
// 结束条件的函数
void _ShowList()
{
cout << endl;
}
//递归函数展开参数包
template <class T, class ...Args>
void _ShowList(T val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
//套一层,封装被添加的模板参数,调用时传参数包即可
template <class ...Args>
void CppPrint(Args... args)
{
_ShowList(args...);
}
int main()
{
CppPrint();
CppPrint(1);
CppPrint(1, 2);
CppPrint(1, 2, 2.2);
CppPrint(1, 2, 2.2, string("xxxx"));
return 0;
}可以看到,我们已经模拟实现了一个printf。
我们先来看代码:
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}是不是一头雾水?没关系,我会详细讲解。
我们看这里初始化数组的列表:{ (PrintArg(args), 0)... }
()里边是逗号表达式,可变参数的…需要写在逗号表达式外面,表示需要将逗号表达式展开,参数包有几个参数,就会生成几个逗号表达式,每个参数都会先后分配到每个逗号表达式中。 { (PrintArg(args), 0)... }就会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0)}0,以确保每个逗号表达式返回的是一个整型值。PrintArg(args),因而达到了参数包展开的目的以list为例:

可以看到,每一个插入接口,在C++11都给配置了一个emplace的插入版本,它相对于普通接口又有何提升呢?

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。
注意:这里不是右值引用,是万能引用。
emplace系列接口的使用方式也普通插入接口使用方式大体相同
示例:
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,可以用构建pair对象的参数后自己去创建一个匿名pair对象,然后用于构造list节点对象
mylist.emplace_back(10, 'a');//支持传入参数包构造
mylist.emplace_back(20, 'b');
//也可以像push_back一样直接用对象构造
mylist.emplace_back(make_pair(30, 'c'));
//不能像push_back一样直接用列表初始化,会报错
mylist.emplace_back({ 60, 'f' });//错误写法
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}我们知道,emplace系列接口调用时,既可以接收左值对象,也可以接收右值对象,还可以接收参数包。
下面我们试一下带有拷贝构造和移动构造的string,以list为例,在emplace_back和push_back接口分别传入左值对象和右值对象,来看看它们在性能上的区别:
分析:
说明:我们在移动构造和构造函数及拷贝构造内部各添加了一个打印提示语,方便我们观察调用情况。

总结:
我们会发现其实差别也不大,emplace_back是直接构造了,push_back是先构造,再移动构造。
我们知道,移动构造的代价是非常小的,相对于深拷贝甚至可以忽略不计。
因此,在需要深拷贝的类中,emplace系列接口价值不大。
但在浅拷贝的类中,更能体现出emplace系列接口的价值,我们来看看在Date类(浅拷贝的类)中,emplace系列接口对性能的提升效果如何? 说明:这里简要实现了一个日期类,在其构造函数和拷贝构造中各打印一条提示语,便于观察。 我们接着测试list的emplace_back和push_back接口:
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
// 构造函数(带默认参数)
// 参数:
// year - 年份(默认1)
// month - 月份(默认1)
// day - 日(默认1)
Date(int year = 1, int month = 1, int day = 1)
:_year(year) // 初始化_year
, _month(month) // 初始化_month
, _day(day) // 初始化_day
{
cout << "Date构造" << endl; // 构造时打印信息
}
// 拷贝构造函数
// 参数:
// d - 要拷贝的Date对象
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date拷贝构造" << endl; // 拷贝构造时打印信息
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
// 可变参数模板函数:创建Date对象
// 模板参数:
// Args - 可变参数类型包
// 函数参数:
// args - 可变参数包(可以匹配0到多个参数)
template <class ...Args>
Date* Create(Args... args)
{
// 使用new在堆上创建Date对象,完美转发args参数
Date* ret = new Date(args...);
return ret; // 返回创建的Date指针
}
int main()
{
std::list<Date> lt;
Date d(2023, 9, 27);
// 只能传日期类对象
lt.push_back(d);
// 传日期类对象
// 传日期类对象的参数包
// 参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象
lt.emplace_back(d);
lt.emplace_back(2023, 9, 27);
return 0;
}
总结:
可以看到,emplace系列接口较普通插入接口,可以少调用一次构造/拷贝构造,当对象较为庞大时,性能的提升是不小的。
最后总结一下emplace系列接口的优势: