C++11之可变参数模板
在 C 的世界里模板机制一直是提升代码复用性和泛型编程能力的重要工具。而可变参数模板Variadic Templates作为 C11 引入的一项强大特性更是将模板的灵活性推向了新的高度。它允许我们编写能够处理任意数量、任意类型参数的函数和类极大地增强了代码的通用性和适应性。一、可变参数模板的基本概念在传统 C 中函数的参数数量和类型是固定的这在很多情况下限制了函数的通用性。而可变参数模板的出现打破了这一限制。它允许函数接受不确定数量的参数这些参数可以是任意类型从而让函数能够以更加灵活的方式处理各种不同的输入。可变参数模板的核心是使用...三个点来表示参数包parameter pack。参数包可以看作是一个包含了多个参数的集合这些参数在函数中可以被逐一处理。例如以下是一个简单的可变参数模板函数的声明代码语言javascriptAI代码解释templatetypename... Args void print(Args... args);在这个例子中Args... args就是一个参数包Args是一个模板参数列表它代表了参数的类型而args是参数包的名称。这个函数可以接受任意数量和类型的参数这些参数在函数体内将被统一处理。二.参数包扩展递归包展开代码语言javascriptAI代码解释void ShowList() { cout endl; } template class T, class ...Args void ShowList(T x, Args... args) { cout x ; ShowList(args...); } template class ...Args void Print(Args... args) { // N个参数第一个传给x剩下N-1参数传给ShowList的第二个参数包 ShowList(args...); }这段代码实现了一个递归打印的功能。Print函数接收一个参数包args然后通过ShowList(args...)的方式将参数包展开并将展开后的结果传递给ShowList函数。ShowList函数通过递归的方式依次打印每个参数直到参数包为空。参数包展开的原理参数包的定义在我们的示例中template class ...Args定义了一个参数包Args它表示Print函数可以接受任意数量和类型的参数。参数包的展开参数包展开是指将参数包中的每个参数依次展开以便在代码中对每个参数进行操作。展开操作是通过...运算符来完成的。在Print函数中ShowList(args...)是参数包展开的关键。这里args是参数包ShowList(args...)的作用是将参数包中的每个参数依次传递给ShowList函数。例如如果调用Print(1, 2.5, hello)那么参数包args包含三个参数1、2.5和hello。ShowList(args...)的展开过程如下第一次调用ShowList(1, 2.5, hello)T是intx是1剩下的参数包args是2.5和hello。第二次调用ShowList(2.5, hello)T是doublex是2.5剩下的参数包args是hello。第三次调用ShowList(hello)T是const char*x是hello剩下的参数包args是空的。最后调用ShowList()参数包为空打印换行符并结束递归。另类包展开首先让我们来看一下本文的核心代码示例代码语言javascriptAI代码解释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 // GetArg的返回值组成实参参数包传给Arguments Arguments(GetArg(args)...); }这段代码中Print函数是关键。它接收一个参数包args然后通过GetArg(args)...的方式将参数包展开并将展开后的结果作为参数传递给Arguments函数。参数包展开的原理参数包的定义template class ...Args定义了一个参数包Args它表示Print函数可以接受任意数量和类型的参数。参数包的展开参数包展开是指将参数包中的每个参数依次展开以便在代码中对每个参数进行操作。展开操作是通过...运算符来完成的。在Print函数中GetArg(args)...是参数包展开的关键。这里args是参数包GetArg(args)表示对参数包中的每个参数调用GetArg函数。...运算符的作用是将GetArg(args)对每个参数的调用结果展开成一个参数列表然后将这个参数列表传递给Arguments函数。例如如果调用Print(1, 2.5, hello)那么参数包args包含三个参数1、2.5和hello。GetArg(args)...的展开过程如下对第一个参数1调用GetArg(1)得到返回值1。对第二个参数2.5调用GetArg(2.5)得到返回值2.5。对第三个参数hello调用GetArg(hello)得到返回值hello。最终GetArg(args)...展开成GetArg(1), GetArg(2.5), GetArg(hello)这相当于1, 2.5, hello然后将这个参数列表传递给Arguments函数。三.emplace系列接口代码语言javascriptAI代码解释template class... Args void emplace_back (Args... args); template class... Args iterator emplace (const_iterator position, Args... args);C11以后STL容器新增了empalce系列的接⼝empalce系列的接⼝均为模板可变参数功能上兼容push和insert系列但是empalce还⽀持新玩法假设容器为containerempalce还⽀持直接插⼊构造T对象的参数这样有些场景会更⾼效⼀些可以直接在容器空间上构造T对象。emplace_back总体⽽⾔是更⾼效推荐以后使⽤emplace系列替代insert和push系列传递参数包过程中如果是 Args… args 的参数包要⽤完美转发参数包⽅式如下std::forward(args)… 否则编译时包扩展后右值引⽤变量表达式就变成了左值。3.1emplace出现的原因在emplace出现之前我们往容器里添加元素时常常会遇到一些效率问题。比如当我们使用std::vector或std::map等容器时如果要添加一个对象通常的做法是先构造好对象然后再将其拷贝或移动到容器中。这个过程涉及到临时对象的创建、拷贝或移动构造等操作不仅代码显得繁琐还可能带来不必要的性能开销尤其是在处理大量数据或复杂对象时这种开销会更加明显。3.2emplace系列接口的登场C11 引入了emplace系列接口为容器操作带来了革命性的变化。emplace的基本思想是在容器分配的存储空间上直接构造对象从而避免了临时对象的创建和拷贝/移动构造等中间步骤。以std::vector为例其emplace_back方法允许我们在向量的末尾直接构造一个对象。假设我们有一个类Person包含姓名和年龄两个成员变量我们想往std::vectorPerson中添加一个Person对象。传统的做法可能是这样代码语言javascriptAI代码解释std::vectorPerson people; Person p(Alice, 25); people.push_back(p);这里Person对象p先被构造出来然后通过push_back方法拷贝到std::vector中涉及到两次构造一次是p的构造一次是拷贝构造到向量中。而使用emplace_back我们可以这样写代码语言javascriptAI代码解释std::vectorPerson people; people.emplace_back(Alice, 25);在这个例子中emplace_back直接在std::vector分配的存储空间上构造了一个Person对象构造参数直接传递给Person的构造函数。这样就避免了临时对象p的创建以及后续的拷贝构造大大提高了效率。3.3emplace系列接口的原理emplace系列接口的魔法在于它利用了完美转发和变长参数模板。当调用emplace或emplace_back等方法时这些方法会将传入的参数完美转发给容器中元素类型的构造函数从而在容器分配的内存空间上直接构造对象。以std::map的emplace方法为例其大致实现原理如下代码语言javascriptAI代码解释template typename Key, typename T, typename Compare, typename Allocator template typename... Args std::pairtypename std::mapKey, T, Compare, Allocator::iterator, bool std::mapKey, T, Compare, Allocator::emplace(Args... args) { // 在合适的位置分配内存 auto position ...; // 完美转发参数给键值对的构造函数在分配的内存上直接构造对象 auto result emplace_hint(position, std::forwardArgs(args)...); return result; }这里的关键是std::forwardArgs(args)...它利用完美转发将参数原封不动地转发给元素类型的构造函数从而保证了参数的值类别左值或右值不会改变使得构造函数能够根据参数的实际类型进行正确的构造。3.4通过代码来看一看emplace的效率代码语言javascriptAI代码解释int main() { // 效率用法都是一样的 bit::listint lt1; lt1.push_back(1); lt1.emplace_back(2); bit::listbit::string lt2; // 传左值效率用法都是一样的 bit::string s1(111111111); lt2.push_back(s1); lt2.emplace_back(s1); cout ***************************************** endl; // 传右值效率用法都是一样的 bit::string s2(111111111); lt2.push_back(move(s2)); bit::string s3(111111111); lt2.emplace_back(move(s3)); cout ***************************************** endl; // emplace_back的效率略高一筹 lt2.push_back(1111111111111111111111111111); lt2.emplace_back(11111111111111111111111111); cout ***************************************** endl; }四.新的类功能4.1 默认的移动构造和移动赋值原来C类中有6个默认成员函数构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const取地址重载最后重要的是前4个后两个⽤处不⼤默认成员函数就是我们不写编译器会⽣成⼀个默认的。C11新增了两个默认成员函数移动构造函数和移动赋值运算符重载。