可变参数模板
序
在之前,如果我们想要实现一个函数可以接收不同类型的参数,我们可以通过模板来实现;那如果想要它接收不同数量的参数怎么办呢?在以前我们可以通过函数重载来实现,但是这种方法非常麻烦,要写多个函数。于是,C++11引入了可变参数模板。
一、基本语法与原理
可变参数模板 ,顾名思义,就是支持可变数量 的函数模板或类模板,即可以传不同数量的参数,可变数量的参数被称为参数包,参数包分为:模板参数包和函数参数包 。它们都能支持传入零到多个 函数参数。
经典形式:
cpp
template <class ...Args> void Func(Args... args) {}
//左值引用
template <class ...Args> void Func(Args&... args) {}
//万能引用
template <class ...Args> void Func(Args&&... args) {}
- 我们用省略号来指出⼀个模板参数或函数参数的表示一个包,在模板参数列表中,
class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。 - 上面的
Args不是一个类型,是一组类型。 - 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 我们可以使用运算符
sizeof...去获取参数包中参数的个数。
cpp
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;
}
可变参数模板是在编译时就确定了参数的类型,上面的调用会结合引用折叠规则实例化出以下四个函数:
cpp
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
二、包扩展
当我们传入参数包时。我们是无法直接去使用其中的变量的,此时我们就引出包扩展以使用包中的参数。
1. 参数包展开(一)------递归展开
cpp
void ShowList()
{
// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
// args是N个参数的参数包
// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
我们来看看它的递归展开图:

再来看一段代码:
cpp
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
if(sizeof...(args)==0)
return;
cout << x << " ";
// args是N个参数的参数包
// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
这是一个典型的错误,可变参数模板的参数匹配是在编译时进行的,而ShowList中的if语句是在参数确定之后才执行的,是运行时逻辑,当args是零个参数时,无法匹配到函数。
2. 参数包展开(二)------利用函数返回值
cpp
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回获得到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
int main()
{
Print(1, 2.2, string("hello"));
return 0;
}
当我们调用Print时,传入的args是<int, double, const char*>,Print内...在GetArg(args)后面,因此展开后得到
cpp
Arguments(
GetArg(1),
GetArg(2.5),
GetArg("hello")
);
然后它就会依次调用GetArgs依次输出。
结果:

我们看到:结果好像反了一下。这是为什么呢?其实像上面的Arguments函数这样的参数为函数返回值的函数,编译器并没有准确规定一定要先执行谁,后执行谁,这全取决于编译器的实现。
如果我们想要正序的输出,可以用第三种初始化列表的方法。
3. 参数包展开(三)------初始化列表
cpp
template<class... Args>
void Print(Args... args)
{
int arr[] = { (cout << args << ' ',0)... };
cout << endl;
}
int main()
{
Print(1, 2.2, string("hello"));
return 0;
}
我们借助了一个数组来依次执行输出,它的值是一个逗号表达式,表达式会先执行第一个参数cout,然后再返回第二个参数0,刚好让数组接收,...前面就是这个表达式,因此将其展开后得到:
cpp
int arr[] =
{
(cout << 1 << ' ', 0),
(cout << 2.2 << ' ', 0),
(cout << string("hello") << ' ', 0)
};
随后arr的值就是[0,0,0]。
这样就能保证编译器依次执行了。
三、emplace
我们以list的emplace_back为例来学习emplace系列接口。
先看定义:

可以看到,list::emplace_back是用万能引用和可变参数模板来实现的。
同样都是插入元素,emplace_back与push_back有什么区别呢?
情况一:左值
cpp
int main()
{
list<zzt::string> lt;
zzt::string s1 = "1111111111";
cout << "********************************" << endl;
lt.push_back(s1);
cout << "********************************" << endl;
lt.emplace_back(s1);
return 0;
}
输出:

可见,传左值时,两者没有任何区别。
情况二:右值
cpp
int main()
{
list<zzt::string> lt;
zzt::string s1 = "1111111111";
zzt::string s2 = "1111111111";
cout << "********************************" << endl;
lt.push_back(move(s1));
cout << "********************************" << endl;
lt.emplace_back(move(s2));
return 0;
}
输出:

可见,传右值时,两者依旧完全相同。
情况三:直接传参
cpp
int main()
{
list<zzt::string> lt;
lt.push_back("1111111111");
cout << "********************************" << endl;
lt.emplace_back("1111111111");
}
输出:

此时就不一样了,push_back是构造+移动构造 ,emplace_back是直接构造 。
我们先来看看两者声明的区别:
push_back:
cpp
void push_back (const value_type& val);
void push_back (value_type&& val);
emplace_back:
cpp
template <class... Args>
void emplace_back (Args&&... args);
对于push_back ,"1111111111"是const char*类型,当我们传入"1111111111"时,由于push_back中的value_type在定义list<zzt::string> lt时就已经确定为zzt::string了,因此,编译器会先构造 一个zzt::string类型的临时对象,再移动构造 给val。
对于emplace_back ,它的参数类型是在编译时展开确定的,因此传入const char*,它的参数类型就是const char*,然后再进行一系列传递,直至传到构造函数,直接构造 。

实际上,两者效率几乎相同,因为移动构造的开销非常非常小。
情况四:多参数类型场景
就以pair为例:
cpp
int main()
{
list<pair<zzt::string, int>> lt;
lt.push_back({ "苹果",1 });
cout<< "********************************" <<endl;
lt.emplace_back("苹果", 1);
return 0;
}
输出:

这个也是不同的,原理和上面的差不多。
对于push_back ,它只能通过花括号传参,不能直接像emplace_back一样通过多个参数来传参。
对于emplace_back,它只能通过多个参数传参,不能使用花括号,因为可变参数模板是在编译时确定类型的,传入一个花括号,它不知道一个花括号代表什么,无法推出参数类型。
总结:部分场景下,emplace可以直接构造,push和insert是构造+移动构造或构造+拷贝构造,在效率方面emplace略胜一筹,emplace综合而言更好用、更强大,因此使用时推荐用emplace替代push和insert。