C++11特性:可变参数模板
前言
可变参数在大家学习C语言之初,写下第一个打印”Hello World“的程序时就接触过了,即printf()。第二个参数 ... ,就是可变参数。
就使用而言,可变参数允许我们一次性传入多个参数,并且类型不限。
但上述可变参数尽针对函数参数,
在C++11中,为了完善泛型编程,引入了可变参数模板,使得模板参数也具备了此特性,能够接受数个不同类型的模板参数。
可变参数模板
语法
形式很简单,只要在模板参数前,带上 ... 就行。
template<class ...Args> //typename也可;省略号跟在class后也行
...Args就是包含各个参数的参数包,可看作一个形似数组的容器,可以存0个或n个参数,不限类型。(形似数组理解,不是数组是因为数组是相同类型元素的集合)
...Args可以作为函数模板参数包,也可作为类模板参数包。
比较
C的可变参数是通过4个宏实现的,且如果定义了一个带可变参数的函数,还必须一起传入可变参数的个数。
但C++的可变模板参数不同,...Args参数包可以直接接收任意个数的任意类型的参数。基础原理就仰仗于函数重载和模板。
原理
这里要先介绍一下sizeof...()操作符,其可计算参数包的参数个数。
为了方便理解其原理,这里介绍函数模板的参数包。
template<class ...Args>
void Print(Args&&... args) {cout << sizeof...(args) << endl;
}int main()
{int x = 1;Print();Print(x);Print(x,2.5);Print(x, 2.5, "Hello");return 0;
}
上文提到其原理仰仗于函数重载和模板。
其实就是,模板使用参数,推导出不同参数列表的函数重载。
上面的代码本质如下:
void Print();void Print(int&& x);void Print(int&& x, double&& y);void Print(int&& x, double&& y, string&& z);
但我们还可以想象,如果没有可变模板参数,我们要让编译器推出上面的函数,
就得写下面1个函数 + 3个函数模板
void Print();template<class T>
void Print(T&& t);template<class T1, class T2>
void Print(T1&& t1, T2&& t2);template<class T1, class T2, class T3>
void Print(T1&& t1, T2&& t2, T3&& t3);
可幸亏有了可变模板参数,在类型泛化的基础上叠加了数量变化,使得泛型编程更加灵活。
包拓展
但到目前为止,我们对参数包有了一定了解,可是如何从参数包中获取参数并使用呢?
sizeof...()操作符远远不够,我们还需要了解包拓展。
void ShowList() {}template<class T, class ...Args>
void ShowList(T t, Args... args)
{cout << t << " ";ShowList(args...);
}template<class ...Args>
void Print(Args... args) {ShowList(args...);
}int main()
{int x = 1;Print(x, 2.5, "Hello");return 0;
}
以上就是包拓展的过程,本质就是用模板帮我们把参数包解构,用 1 + n-1 的函数模板重载 n 个函数,但是要自行处理没有参数的函数重载。
但实际上,这个玩法已经过时了,在C++17中已经有了更高级的玩法,感兴趣可以去看看文档,问问AI。
不过其实还有一种玩法,只不过有点抽象,需要体会。。
template<class T>
const T& getArgs(const T& t)
{cout << t << " ";return t;
}template<class ...Args>
void Arguments(Args... args)
{}template<class ...Args>
void Print(Args... args) {Arguments(getArgs(args)...);//用getArgs的返回值构成Arguments()的参数包
}int main()
{int x = 1;Print(x, 2.5, "Hello");return 0;
}
本质如下
void Print(int x, string y, double z)
{Arguments(getArgs(z), getArgs(y), getArgs(x));
}
怎么是倒序?C++规定是这样的,参数包展开时的求值顺序是从右到左的。 规则是别人定的。
要正序其实还是离不开上一种形似递归(bushi)的方法。
应用(emplace系列)
C++11给STL容器都新增了一个接口emplace
瞩目的点当然就是这个可变参数模板了,怎么好端端的push_back,insert不够香吗?
接下来会以list的emplace_back进行解释。
对比emplace与push系列
一般在定义list时,我们其实都已经确定好了模板参数,如下。
list<string> lt;
那么在push_back时,该函数模板也被实例化为
void push_back(const string& val);lt.push_back("666");
如上场景中,"666"是一个const char*型的常量字符串,
就需要先隐式类型转换,构造一个string临时对象,再将临时对象拷贝构造给结点数据域的string,最后尾插。
看起来没什么,请看如下场景:
template <class... Args>void emplace_back (Args&&... args);lt.emplace_back("666");
同样是const char*的常量字符串,push_back()由于实例化好了,要进行隐式转换
而emplac_back()只有在调用、接受参数后,编译器才会实例化它,并且参数类型是const char*!
这有差别吗?有的,有的。
有了 const char* 的常量字符串,可以直接再将其传给结点的可变参数模板的构造函数,此时又可以继续将const char* 常量字符串作为参数去直接构造结点的string成员变量,走string(const char* s)的构造,只需要构造一次就够了!
好了,我知道这段话很长很臭,请看下图:
图中实现了一路将常量字符串传递给了结点string的构造,只要进行一次构造就够了。
而相对于push_back(),还要不断地拷贝构造,这个效率在对比下就体现出来了。
(补充:emplace_back的底层可以理解为又去复用了一个可变参数模板的insert(),中间要用完美转发传递右值参数)
template <class... Args>
void emplace_back(Args&&... args)
{insert(end(), std::forward<Args>(args)...);//!!!
}template <class... Args>
iterator insert(iterator pos, Args&&... args)
{Node* cur = pos._node;Node* newnode = new Node(std::forward<Args>(args)...);//!!!Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);
}
但上述是否能证明emplace系列就一定比push系列、insert()高效呢?
NO。
如果是如下情形,
string s("666");//左值传参
lt.push_back(s);lt.emplace_back(s);//右值传参
lt.push_back((move)s);lt.emplace_back((move)s);
此时二者的效率就没差别了,都是字符串,要么拷贝构造、要么移动拷贝。
总结
总结一下,emplace系列在接受到构造函数所需参数时,能够直接构造,中间不需要拷贝构造。如此情形下,效率较高;而其他情形的效率没差。(读者可以用list<pair<string, int>>自行感受)
但毕竟emplace存在过人之处,哪怕只有一个,也是过人之处。
所以以后还是推荐用emplace系列。
上述涉及”移动语义“与”右值“的前置知识,建议学完再看emplace部分。。。
----------------------------------------------------------完--------------------------------------------------------------------