可变参数模板
- 序
- 一、基本语法与原理
- 二、包扩展
- 1. 参数包展开(一)——递归展开
- 2. 参数包展开(二)——利用函数返回值
- 3. 参数包展开(三)——初始化列表
- 三、emplace
- 情况一:左值
- 情况二:右值
- 情况三:直接传参
- 情况四:多参数类型场景
序
在之前,如果我们想要实现一个函数可以接收不同类型的参数,我们可以通过模板来实现;那如果想要它接收不同数量的参数怎么办呢?在以前我们可以通过函数重载来实现,但是这种方法非常麻烦,要写多个函数。于是,C++11引入了可变参数模板。
一、基本语法与原理
可变参数模板,顾名思义,就是支持可变数量的函数模板或类模板,即可以传不同数量的参数,可变数量的参数被称为参数包,参数包分为:模板参数包和函数参数包。它们都能支持传入零到多个函数参数。
经典形式:
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...去获取参数包中参数的个数。
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; }可变参数模板是在编译时就确定了参数的类型,上面的调用会结合引用折叠规则实例化出以下四个函数:
void Print(); void Print(int&& arg1); void Print(int&& arg1, string&& arg2); void Print(double&& arg1, string&& arg2, double& arg3);二、包扩展
当我们传入参数包时。我们是无法直接去使用其中的变量的,此时我们就引出包扩展以使用包中的参数。
1. 参数包展开(一)——递归展开
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...); }我们来看看它的递归展开图:
再来看一段代码:
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. 参数包展开(二)——利用函数返回值
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)后面,因此展开后得到
Arguments( GetArg(1), GetArg(2.5), GetArg("hello") );然后它就会依次调用GetArgs依次输出。
结果:
我们看到:结果好像反了一下。这是为什么呢?其实像上面的Arguments函数这样的参数为函数返回值的函数,编译器并没有准确规定一定要先执行谁,后执行谁,这全取决于编译器的实现。
如果我们想要正序的输出,可以用第三种初始化列表的方法。
3. 参数包展开(三)——初始化列表
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,刚好让数组接收,...前面就是这个表达式,因此将其展开后得到:
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有什么区别呢?
情况一:左值
int main() { list<zzt::string> lt; zzt::string s1 = "1111111111"; cout << "********************************" << endl; lt.push_back(s1); cout << "********************************" << endl; lt.emplace_back(s1); return 0; }输出:
可见,传左值时,两者没有任何区别。
情况二:右值
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; }输出:
可见,传右值时,两者依旧完全相同。
情况三:直接传参
int main() { list<zzt::string> lt; lt.push_back("1111111111"); cout << "********************************" << endl; lt.emplace_back("1111111111"); }输出:
此时就不一样了,push_back是构造+移动构造,emplace_back是直接构造。
我们先来看看两者声明的区别:
push_back:
void push_back (const value_type& val); void push_back (value_type&& val);emplace_back:
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为例:
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。