大家好欢迎来到 huangjin007_ 的博客⭐个人主页huangjin007_文章收录专栏零基础入门C总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇C11篇(三) —— 可变参数模版详解本篇文章将由浅入深带你循序渐进地掌握C11中的可变参数模版全程干货坐稳发车~ ദ്ദി˶̀֊́ )✧文章目录C11篇(三) —— 可变参数模版详解1. 什么是可变参数模板2. 基本语法与原理2.1 声明一个可变参数函数模板2.2 获取参数包的大小sizeof...2.3 实例化原理——编译器悄悄做了重载2.4 不能用循环或下标访问参数包3. 包扩展参数包展开3.1 递归展开3.2 更灵活的模式扩展直接作为实参列表3.3 包扩展的通用理解4. emplace 系列接口 —— 直接构造高效插入4.1 功能对比push_back vs emplace_back4.2 实现原理完美转发参数包4.3 与 push_back 的调用路径对比5. 面试常见考点5.1 面试可能被问到的问题结语1. 什么是可变参数模板C11 引入的可变参数模板简单说就是让函数模板和类模板可以接受任意数量、任意类型的参数。以前我们只能为每种参数个数写一个重载现在一个模板就能搞定。比如templateclass...ArgsvoidFunc(Args...args){}这里Args就是一个模板参数包可以代表 0 个或多个模板类型参数而args是函数参数包对应着 0 个或多个函数参数。和普通模板参数不一样参数包前面有省略号...。class... Args表示Args是一组类型的集合可以没有也可以有很多。Args... args则是一组值的集合。2. 基本语法与原理2.1 声明一个可变参数函数模板看几个常见的写法// 1. 按值传递参数包templateclass...ArgsvoidFunc(Args...args){}// 2. 左值引用templateclass...ArgsvoidFunc(Args...args){}// 3. 万能引用也叫转发引用templateclass...ArgsvoidFunc(Args...args){}重点解释一下第三种Args... args。这里的不是单纯的右值引用因为Args是模板参数包在类型推导时会发生引用折叠如果传入左值int xArgs被推导为int折叠后int -int所以参数是左值引用。如果传入右值string(xxx)Args被推导为string那么就是string绑定右值。这样一来第三种写法几乎可以接收任何类型的实参同时保留其左右值属性非常灵活。2.2 获取参数包的大小sizeof…我们可以用sizeof...(args)来获取参数包中元素的个数它是一个编译期常量。一个简单的例子templateclass...ArgsvoidPrint(Args...args){coutsizeof...(args)endl;// 计算参数包中参数的个数}intmain(){doublex1.11;Print();// 包里有0个参数Print(1);// 包里有1个参数Print(1,string(xxx));// 包里有2个参数Print(1,string(xxx),x);// 包里有3个参数return0;}2.3 实例化原理——编译器悄悄做了重载可变参数模板并不是运行时动态处理参数而是编译时通过模板实例化生成一个个具体参数个数的重载函数。以上面Print为例编译器会根据调用实例化出以下四个函数应用引用折叠规则后voidPrint();// 无参数voidPrint(intarg1);// 一个右值voidPrint(intarg1,stringarg2);// 两个右值voidPrint(intarg1,stringarg2,doublearg3);// 两个右值一个左值引用从本质上说可变参数模板只是在类型泛化的基础上又给我们叠加了一层“数量泛化”。没有它我们得手动写一堆重载voidPrint();templateclassT1voidPrint(T1arg1);templateclassT1,classT2voidPrint(T1arg1,T2arg2);templateclassT1,classT2,classT3voidPrint(T1arg1,T2arg2,T3arg3);// 还有更多...有了可变参数模板一个templateclass... Args void Print(Args... args)就全搞定了不仅代码量骤减而且扩展性大大提高。2.4 不能用循环或下标访问参数包很多人会尝试这样访问参数包// 错误示例1范围fortemplateclass...ArgsvoidPrint(Args...args){for(autoe:args...)// 错误参数包不是容器不能这样遍历coute ;}或者// 错误示例2下标访问templateclass...ArgsvoidPrint(Args...args){for(size_t i0;isizeof...(args);i)coutargs[i] ;// 错误args不是数组没有下标操作}这两种想法很自然但都是错误的。原因在于参数包是编译时概念在代码编译后args...就变成了具体的多个参数根本不存在一个运行时的集合让你遍历。下标操作需要运行期索引而每个参数的类型可能不同编译器无法在运行期才去决定访问哪个类型。那我们想处理每一个参数该怎么办答案就是下一节的重头戏——包扩展。3. 包扩展参数包展开包扩展是我们能对参数包做的唯一“处理动作”。扩展一个包就是把它拆成一个一个的元素并对每个元素应用某个模式最后生成一个逗号分隔的列表。基本语法就是在模式的右边放一个省略号...来触发扩展。举个直观的模式GetArg(args)...如果args是x, y, z就会扩展成GetArg(x), GetArg(y), GetArg(z)。3.1 递归展开最常见的包扩展方式是用递归函数每次从参数包里获得第一个参数剩下的参数包继续递归直到包为空时调用一个终止函数。看例子// 递归终止函数参数包为空时匹配此版本voidShowList(){coutendl;}// 递归函数每次输出第一个参数剩余参数包继续递归templateclassT,class...ArgsvoidShowList(Tx,Args...args){coutx ;ShowList(args...);// 包扩展将 args 原封不动地展开传递给下一次调用}// 对外接口转发参数包templateclass...ArgsvoidPrint(Args...args){ShowList(args...);}调用Print(1, string(xxxx), 2.2);时发生了什么我们来跟踪编译器的推导过程Print(1, string(xxxx), 2.2)调用ShowList(args...)此时args...是1, string(xxxx), 2.2。第一次匹配ShowList(T x, Args... args)T推导为intx绑定到1剩余的Args推导为[string, double]参数包args包含string(xxxx)和2.2。输出1后调用ShowList(string(xxxx), 2.2)。第二次匹配T推导为stringx绑定xxxx剩余Args为doubleargs包含2.2。输出xxxx后调用ShowList(2.2)。第三次匹配T推导为doublex绑定2.2剩余Args为空包。输出2.2后调用ShowList()。ShowList()匹配到无参的终止版本输出换行。整个过程中编译器递归地生成了三份ShowList的实例// 实例1三参数版本 int, string, doublevoidShowList(intx,stringa,doubleb){coutx ;ShowList(a,b);// 传给两参数版本}// 实例2两参数版本 string, doublevoidShowList(stringx,doubleb){coutx ;ShowList(b);// 传给单参数版本}// 实例3单参数版本 doublevoidShowList(doublex){coutx ;ShowList();// 传给空参终止函数}而顶层的Print实质上变成了voidPrint(intx,string y,doublez){ShowList(x,y,z);}这样通过编译时递归我们实现了对每个参数的处理。整个过程没有循环没有运行期开销全部在编译期完成。3.2 更灵活的模式扩展直接作为实参列表包扩展不仅能在递归中传递包本身还可以对一个函数调用应用模式比如把所有元素经过某个函数转换后再传给另一个函数。看这个例子templateclassTconstTGetArg(constTx){coutx ;returnx;}templateclass...ArgsvoidArguments(Args...args){}templateclass...ArgsvoidPrint(Args...args){// 对 args 中每一个元素调用 GetArg然后把所有返回值组成参数包传给 ArgumentsArguments(GetArg(args)...);}当调用Print(1, string(xxxx), 1.1);时包扩展GetArg(args)...会展开成Arguments(GetArg(1),GetArg(string(xxxx)),GetArg(1.1));所以GetArg会被依次调用输出每个参数并返回它的引用这些返回值再作为实参传给Arguments。编译器最终会将该模板实例化为类似这样的函数voidPrint(intx,string y,doublez){Arguments(GetArg(x),GetArg(y),GetArg(z));}这种模式非常强大可以用在各种需要对参数包进行逐元素转换的场景。3.3 包扩展的通用理解一句话总结扩展一个包就是把它拆开每个元素套上左边的模式最后拼成一个逗号分隔的列表。常见形式args...直接展开成arg1, arg2, arg3args...展开成arg1, arg2, arg3取地址func(args)...展开成func(arg1), func(arg2), func(arg3)std::forwardArgs(args)...展开成std::forwardArg1(arg1), std::forwardArg2(arg2), ...完美转发4. emplace 系列接口 —— 直接构造高效插入C11 给 STL 容器增加了emplace系列接口比如vector、list都支持templateclass...Argsvoidemplace_back(Args...args);templateclass...Argsiteratoremplace(const_iterator position,Args...args);4.1 功能对比push_back vs emplace_backemplace_back不仅兼容push_back的全部功能还多了一个“绝活”可以直接传递构造元素所需的参数在容器内部直接构造对象省去临时对象的创建和移动/拷贝。这里我们用之前模拟实现的命名空间为 hj 的string类来测试listhj::stringlt;hj::strings1(1111);// 1. 传左值 —— 与 push_back 一样都是拷贝构造lt.emplace_back(s1);// 一次拷贝构造lt.push_back(s1);// 一次拷贝构造// 2. 传右值 —— 与 push_back 一样都是移动构造lt.emplace_back(move(s1));// 一次移动构造lt.push_back(move(s1));// 一次移动构造// 3. 直接传字符串字面量 1111 —— push_back 做不到这种lt.emplace_back(1111);// 内部直接用 1111 作为参数在节点构造时调用 hj::string(const char*)一步到位// 而 push_back(1111) 必须先隐式转换出一个临时 hj::string// 再移动/拷贝到容器多一次临时对象的构造和析构lt.push_back(1111);对于pair这类复合类型优势更明显listpairhj::string,intlt1;pairhj::string,intkv(苹果,1);// 正常传 pair 对象lt1.emplace_back(kv);// 拷贝构造lt1.emplace_back(move(kv));// 移动构造// 而 emplace_back 允许直接传 pair 的构造参数lt1.emplace_back(苹果,1);// 节点内直接调用 pair hj::string, int (苹果, 1)原地构造这种方式不仅少写了make_pair或临时对象效率也更高减少了不必要的临时对象产生。4.2 实现原理完美转发参数包emplace_back为什么这么厉害我们模拟实现一个简化的list看看它的内部是如何工作的。核心在于可变参数模板接收任意构造参数通过std::forward完美转发这些参数到节点的构造函数节点构造函数再转发给存储类型的构造函数直接原地构造。代码骨架如下namespacehj{templateclassTstructListNode{ListNodeT*_next;ListNodeT*_prev;T _data;ListNode(Tdata):_next(nullptr),_prev(nullptr),_data(move(data)){}templateclass...ArgsListNode(Args...args):_next(nullptr),_prev(nullptr),_data(std::forwardArgs(args)...){}};templateclassT,classRef,classPtrstructListIterator{typedefListNodeTNode;typedefListIteratorT,Ref,PtrSelf;Node*_node;ListIterator(Node*node):_node(node){}// it;Selfoperator(){_node_node-_next;return*this;}Selfoperator--(){_node_node-_prev;return*this;}Refoperator*(){return_node-_data;}booloperator!(constSelfit){return_node!it._node;}};templateclassTclasslist{typedefListNodeTNode;public:typedefListIteratorT,T,T*iterator;typedefListIteratorT,constT,constT*const_iterator;iteratorbegin(){returniterator(_head-_next);}iteratorend(){returniterator(_head);}voidempty_init(){_headnewNode();_head-_next_head;_head-_prev_head;}list(){empty_init();}voidpush_back(constTx){insert(end(),x);}voidpush_back(Tx){insert(end(),move(x));}iteratorinsert(iterator pos,constTx){Node*curpos._node;Node*newnodenewNode(x);Node*prevcur-_prev;// prev newnode curprev-_nextnewnode;newnode-_prevprev;newnode-_nextcur;cur-_prevnewnode;returniterator(newnode);}iteratorinsert(iterator pos,Tx){Node*curpos._node;Node*newnodenewNode(move(x));Node*prevcur-_prev;prev-_nextnewnode;newnode-_prevprev;newnode-_nextcur;cur-_prevnewnode;returniterator(newnode);}templateclass...Argsvoidemplace_back(Args...args){insert(end(),std::forwardArgs(args)...);}templateclass...Argsiteratorinsert(iterator pos,Args...args){Node*curpos._node;Node*newnodenewNode(std::forwardArgs(args)...);Node*prevcur-_prev;// prev newnode curprev-_nextnewnode;newnode-_prevprev;newnode-_nextcur;cur-_prevnewnode;returniterator(newnode);}private:Node*_head;};}注意转发时的写法是std::forwardArgs(args)...这行代码会展开成std::forwardArg1(arg1),std::forwardArg2(arg2),...为什么必须用std::forward因为Args... args是万能引用传入的左值会推导为左值引用右值会推导为右值引用。但形参args本身是个变量它一定是左值。如果不加forward参数包展开后直接传args...那么所有参数都会变成左值丢失了原始的值类别。完美转发保证了你传左值我就给你左值你传右值我就给你右值原封不动地传递下去。4.3 与 push_back 的调用路径对比以lt.emplace_back(1111)和lt.push_back(1111)为例push_back 路径1111首先隐式转换成临时string对象调用string(const char*)构造。push_back(const T)或push_back(T)接收这个临时对象。在节点内调用data的拷贝或移动构造。析构临时对象。emplace_back 路径emplace_back接收const char*参数。通过完美转发将const char*原样传给节点的构造函数。节点构造函数在初始化列表里直接调用string(const char*)构造data。完全没有临时对象。很明显emplace_back少了一次构造和一次析构所以效率更高。尤其在构造对象代价高昂的时候提升非常可观。5. 面试常见考点5.1 面试可能被问到的问题Q1可变参数模板如何展开参数包A编译器通过递归模板实例化每次分离出第一个参数和剩余参数包直到包为空时调用终止函数。这是编译时递归不依赖运行期循环。Q2sizeof...(args)和普通sizeof有什么区别Asizeof...是专门用于参数包的运算符返回参数个数是编译期常量。普通sizeof用于求类型或表达式的字节大小。Q3为什么不能用循环遍历参数包A参数包是编译时概念编译实例化后就是独立的多个参数不存在运行时可遍历的集合且各个参数类型可能不同无法用统一的循环体处理。Q4emplace_back比push_back快吗什么时候快A当你直接传递构造参数而非现成对象时emplace_back可以避免创建临时对象从而减少一次拷贝/移动构造和析构。如果你已经有一个现成的对象两者性能一样。Q5完美转发参数包的写法及必要性A写法是std::forwardArgs(args)...。必要性因为形参都是左值不转发就丢失了右值信息导致本该移动的变成拷贝。结语今天的内容到这里就结束了希望你能有所收获~干货整理到手抖觉得有用的话赏个三连回回血__(:ᗤ」ㄥ)_ _
【C++11篇(三)】可变参数模版详解
发布时间:2026/7/7 3:25:54
大家好欢迎来到 huangjin007_ 的博客⭐个人主页huangjin007_文章收录专栏零基础入门C总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇C11篇(三) —— 可变参数模版详解本篇文章将由浅入深带你循序渐进地掌握C11中的可变参数模版全程干货坐稳发车~ ദ്ദി˶̀֊́ )✧文章目录C11篇(三) —— 可变参数模版详解1. 什么是可变参数模板2. 基本语法与原理2.1 声明一个可变参数函数模板2.2 获取参数包的大小sizeof...2.3 实例化原理——编译器悄悄做了重载2.4 不能用循环或下标访问参数包3. 包扩展参数包展开3.1 递归展开3.2 更灵活的模式扩展直接作为实参列表3.3 包扩展的通用理解4. emplace 系列接口 —— 直接构造高效插入4.1 功能对比push_back vs emplace_back4.2 实现原理完美转发参数包4.3 与 push_back 的调用路径对比5. 面试常见考点5.1 面试可能被问到的问题结语1. 什么是可变参数模板C11 引入的可变参数模板简单说就是让函数模板和类模板可以接受任意数量、任意类型的参数。以前我们只能为每种参数个数写一个重载现在一个模板就能搞定。比如templateclass...ArgsvoidFunc(Args...args){}这里Args就是一个模板参数包可以代表 0 个或多个模板类型参数而args是函数参数包对应着 0 个或多个函数参数。和普通模板参数不一样参数包前面有省略号...。class... Args表示Args是一组类型的集合可以没有也可以有很多。Args... args则是一组值的集合。2. 基本语法与原理2.1 声明一个可变参数函数模板看几个常见的写法// 1. 按值传递参数包templateclass...ArgsvoidFunc(Args...args){}// 2. 左值引用templateclass...ArgsvoidFunc(Args...args){}// 3. 万能引用也叫转发引用templateclass...ArgsvoidFunc(Args...args){}重点解释一下第三种Args... args。这里的不是单纯的右值引用因为Args是模板参数包在类型推导时会发生引用折叠如果传入左值int xArgs被推导为int折叠后int -int所以参数是左值引用。如果传入右值string(xxx)Args被推导为string那么就是string绑定右值。这样一来第三种写法几乎可以接收任何类型的实参同时保留其左右值属性非常灵活。2.2 获取参数包的大小sizeof…我们可以用sizeof...(args)来获取参数包中元素的个数它是一个编译期常量。一个简单的例子templateclass...ArgsvoidPrint(Args...args){coutsizeof...(args)endl;// 计算参数包中参数的个数}intmain(){doublex1.11;Print();// 包里有0个参数Print(1);// 包里有1个参数Print(1,string(xxx));// 包里有2个参数Print(1,string(xxx),x);// 包里有3个参数return0;}2.3 实例化原理——编译器悄悄做了重载可变参数模板并不是运行时动态处理参数而是编译时通过模板实例化生成一个个具体参数个数的重载函数。以上面Print为例编译器会根据调用实例化出以下四个函数应用引用折叠规则后voidPrint();// 无参数voidPrint(intarg1);// 一个右值voidPrint(intarg1,stringarg2);// 两个右值voidPrint(intarg1,stringarg2,doublearg3);// 两个右值一个左值引用从本质上说可变参数模板只是在类型泛化的基础上又给我们叠加了一层“数量泛化”。没有它我们得手动写一堆重载voidPrint();templateclassT1voidPrint(T1arg1);templateclassT1,classT2voidPrint(T1arg1,T2arg2);templateclassT1,classT2,classT3voidPrint(T1arg1,T2arg2,T3arg3);// 还有更多...有了可变参数模板一个templateclass... Args void Print(Args... args)就全搞定了不仅代码量骤减而且扩展性大大提高。2.4 不能用循环或下标访问参数包很多人会尝试这样访问参数包// 错误示例1范围fortemplateclass...ArgsvoidPrint(Args...args){for(autoe:args...)// 错误参数包不是容器不能这样遍历coute ;}或者// 错误示例2下标访问templateclass...ArgsvoidPrint(Args...args){for(size_t i0;isizeof...(args);i)coutargs[i] ;// 错误args不是数组没有下标操作}这两种想法很自然但都是错误的。原因在于参数包是编译时概念在代码编译后args...就变成了具体的多个参数根本不存在一个运行时的集合让你遍历。下标操作需要运行期索引而每个参数的类型可能不同编译器无法在运行期才去决定访问哪个类型。那我们想处理每一个参数该怎么办答案就是下一节的重头戏——包扩展。3. 包扩展参数包展开包扩展是我们能对参数包做的唯一“处理动作”。扩展一个包就是把它拆成一个一个的元素并对每个元素应用某个模式最后生成一个逗号分隔的列表。基本语法就是在模式的右边放一个省略号...来触发扩展。举个直观的模式GetArg(args)...如果args是x, y, z就会扩展成GetArg(x), GetArg(y), GetArg(z)。3.1 递归展开最常见的包扩展方式是用递归函数每次从参数包里获得第一个参数剩下的参数包继续递归直到包为空时调用一个终止函数。看例子// 递归终止函数参数包为空时匹配此版本voidShowList(){coutendl;}// 递归函数每次输出第一个参数剩余参数包继续递归templateclassT,class...ArgsvoidShowList(Tx,Args...args){coutx ;ShowList(args...);// 包扩展将 args 原封不动地展开传递给下一次调用}// 对外接口转发参数包templateclass...ArgsvoidPrint(Args...args){ShowList(args...);}调用Print(1, string(xxxx), 2.2);时发生了什么我们来跟踪编译器的推导过程Print(1, string(xxxx), 2.2)调用ShowList(args...)此时args...是1, string(xxxx), 2.2。第一次匹配ShowList(T x, Args... args)T推导为intx绑定到1剩余的Args推导为[string, double]参数包args包含string(xxxx)和2.2。输出1后调用ShowList(string(xxxx), 2.2)。第二次匹配T推导为stringx绑定xxxx剩余Args为doubleargs包含2.2。输出xxxx后调用ShowList(2.2)。第三次匹配T推导为doublex绑定2.2剩余Args为空包。输出2.2后调用ShowList()。ShowList()匹配到无参的终止版本输出换行。整个过程中编译器递归地生成了三份ShowList的实例// 实例1三参数版本 int, string, doublevoidShowList(intx,stringa,doubleb){coutx ;ShowList(a,b);// 传给两参数版本}// 实例2两参数版本 string, doublevoidShowList(stringx,doubleb){coutx ;ShowList(b);// 传给单参数版本}// 实例3单参数版本 doublevoidShowList(doublex){coutx ;ShowList();// 传给空参终止函数}而顶层的Print实质上变成了voidPrint(intx,string y,doublez){ShowList(x,y,z);}这样通过编译时递归我们实现了对每个参数的处理。整个过程没有循环没有运行期开销全部在编译期完成。3.2 更灵活的模式扩展直接作为实参列表包扩展不仅能在递归中传递包本身还可以对一个函数调用应用模式比如把所有元素经过某个函数转换后再传给另一个函数。看这个例子templateclassTconstTGetArg(constTx){coutx ;returnx;}templateclass...ArgsvoidArguments(Args...args){}templateclass...ArgsvoidPrint(Args...args){// 对 args 中每一个元素调用 GetArg然后把所有返回值组成参数包传给 ArgumentsArguments(GetArg(args)...);}当调用Print(1, string(xxxx), 1.1);时包扩展GetArg(args)...会展开成Arguments(GetArg(1),GetArg(string(xxxx)),GetArg(1.1));所以GetArg会被依次调用输出每个参数并返回它的引用这些返回值再作为实参传给Arguments。编译器最终会将该模板实例化为类似这样的函数voidPrint(intx,string y,doublez){Arguments(GetArg(x),GetArg(y),GetArg(z));}这种模式非常强大可以用在各种需要对参数包进行逐元素转换的场景。3.3 包扩展的通用理解一句话总结扩展一个包就是把它拆开每个元素套上左边的模式最后拼成一个逗号分隔的列表。常见形式args...直接展开成arg1, arg2, arg3args...展开成arg1, arg2, arg3取地址func(args)...展开成func(arg1), func(arg2), func(arg3)std::forwardArgs(args)...展开成std::forwardArg1(arg1), std::forwardArg2(arg2), ...完美转发4. emplace 系列接口 —— 直接构造高效插入C11 给 STL 容器增加了emplace系列接口比如vector、list都支持templateclass...Argsvoidemplace_back(Args...args);templateclass...Argsiteratoremplace(const_iterator position,Args...args);4.1 功能对比push_back vs emplace_backemplace_back不仅兼容push_back的全部功能还多了一个“绝活”可以直接传递构造元素所需的参数在容器内部直接构造对象省去临时对象的创建和移动/拷贝。这里我们用之前模拟实现的命名空间为 hj 的string类来测试listhj::stringlt;hj::strings1(1111);// 1. 传左值 —— 与 push_back 一样都是拷贝构造lt.emplace_back(s1);// 一次拷贝构造lt.push_back(s1);// 一次拷贝构造// 2. 传右值 —— 与 push_back 一样都是移动构造lt.emplace_back(move(s1));// 一次移动构造lt.push_back(move(s1));// 一次移动构造// 3. 直接传字符串字面量 1111 —— push_back 做不到这种lt.emplace_back(1111);// 内部直接用 1111 作为参数在节点构造时调用 hj::string(const char*)一步到位// 而 push_back(1111) 必须先隐式转换出一个临时 hj::string// 再移动/拷贝到容器多一次临时对象的构造和析构lt.push_back(1111);对于pair这类复合类型优势更明显listpairhj::string,intlt1;pairhj::string,intkv(苹果,1);// 正常传 pair 对象lt1.emplace_back(kv);// 拷贝构造lt1.emplace_back(move(kv));// 移动构造// 而 emplace_back 允许直接传 pair 的构造参数lt1.emplace_back(苹果,1);// 节点内直接调用 pair hj::string, int (苹果, 1)原地构造这种方式不仅少写了make_pair或临时对象效率也更高减少了不必要的临时对象产生。4.2 实现原理完美转发参数包emplace_back为什么这么厉害我们模拟实现一个简化的list看看它的内部是如何工作的。核心在于可变参数模板接收任意构造参数通过std::forward完美转发这些参数到节点的构造函数节点构造函数再转发给存储类型的构造函数直接原地构造。代码骨架如下namespacehj{templateclassTstructListNode{ListNodeT*_next;ListNodeT*_prev;T _data;ListNode(Tdata):_next(nullptr),_prev(nullptr),_data(move(data)){}templateclass...ArgsListNode(Args...args):_next(nullptr),_prev(nullptr),_data(std::forwardArgs(args)...){}};templateclassT,classRef,classPtrstructListIterator{typedefListNodeTNode;typedefListIteratorT,Ref,PtrSelf;Node*_node;ListIterator(Node*node):_node(node){}// it;Selfoperator(){_node_node-_next;return*this;}Selfoperator--(){_node_node-_prev;return*this;}Refoperator*(){return_node-_data;}booloperator!(constSelfit){return_node!it._node;}};templateclassTclasslist{typedefListNodeTNode;public:typedefListIteratorT,T,T*iterator;typedefListIteratorT,constT,constT*const_iterator;iteratorbegin(){returniterator(_head-_next);}iteratorend(){returniterator(_head);}voidempty_init(){_headnewNode();_head-_next_head;_head-_prev_head;}list(){empty_init();}voidpush_back(constTx){insert(end(),x);}voidpush_back(Tx){insert(end(),move(x));}iteratorinsert(iterator pos,constTx){Node*curpos._node;Node*newnodenewNode(x);Node*prevcur-_prev;// prev newnode curprev-_nextnewnode;newnode-_prevprev;newnode-_nextcur;cur-_prevnewnode;returniterator(newnode);}iteratorinsert(iterator pos,Tx){Node*curpos._node;Node*newnodenewNode(move(x));Node*prevcur-_prev;prev-_nextnewnode;newnode-_prevprev;newnode-_nextcur;cur-_prevnewnode;returniterator(newnode);}templateclass...Argsvoidemplace_back(Args...args){insert(end(),std::forwardArgs(args)...);}templateclass...Argsiteratorinsert(iterator pos,Args...args){Node*curpos._node;Node*newnodenewNode(std::forwardArgs(args)...);Node*prevcur-_prev;// prev newnode curprev-_nextnewnode;newnode-_prevprev;newnode-_nextcur;cur-_prevnewnode;returniterator(newnode);}private:Node*_head;};}注意转发时的写法是std::forwardArgs(args)...这行代码会展开成std::forwardArg1(arg1),std::forwardArg2(arg2),...为什么必须用std::forward因为Args... args是万能引用传入的左值会推导为左值引用右值会推导为右值引用。但形参args本身是个变量它一定是左值。如果不加forward参数包展开后直接传args...那么所有参数都会变成左值丢失了原始的值类别。完美转发保证了你传左值我就给你左值你传右值我就给你右值原封不动地传递下去。4.3 与 push_back 的调用路径对比以lt.emplace_back(1111)和lt.push_back(1111)为例push_back 路径1111首先隐式转换成临时string对象调用string(const char*)构造。push_back(const T)或push_back(T)接收这个临时对象。在节点内调用data的拷贝或移动构造。析构临时对象。emplace_back 路径emplace_back接收const char*参数。通过完美转发将const char*原样传给节点的构造函数。节点构造函数在初始化列表里直接调用string(const char*)构造data。完全没有临时对象。很明显emplace_back少了一次构造和一次析构所以效率更高。尤其在构造对象代价高昂的时候提升非常可观。5. 面试常见考点5.1 面试可能被问到的问题Q1可变参数模板如何展开参数包A编译器通过递归模板实例化每次分离出第一个参数和剩余参数包直到包为空时调用终止函数。这是编译时递归不依赖运行期循环。Q2sizeof...(args)和普通sizeof有什么区别Asizeof...是专门用于参数包的运算符返回参数个数是编译期常量。普通sizeof用于求类型或表达式的字节大小。Q3为什么不能用循环遍历参数包A参数包是编译时概念编译实例化后就是独立的多个参数不存在运行时可遍历的集合且各个参数类型可能不同无法用统一的循环体处理。Q4emplace_back比push_back快吗什么时候快A当你直接传递构造参数而非现成对象时emplace_back可以避免创建临时对象从而减少一次拷贝/移动构造和析构。如果你已经有一个现成的对象两者性能一样。Q5完美转发参数包的写法及必要性A写法是std::forwardArgs(args)...。必要性因为形参都是左值不转发就丢失了右值信息导致本该移动的变成拷贝。结语今天的内容到这里就结束了希望你能有所收获~干货整理到手抖觉得有用的话赏个三连回回血__(:ᗤ」ㄥ)_ _