◆ 博主名称 晓此方-CSDN博客大家好欢迎来到晓此方的博客。⭐️现代C系列个人专栏 插曲现代C⭐️Re系列专栏我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)文章目录概要序論一模板中的模板可变参数模板1.1从概念开始——什么是可变参数与参数包1.1.1从C库中的printf引入1.1.2模板的可变参数Tips新操作符追加sizeof...1.2简化代码的利器——可变参数模板该怎么用1.2.1 递归函数方式展开参数包包扩展1.2.2包扩展的终止函数的设计为何重要1.2.3强化对参数包的理解——另外一种调用场景分析1.2.4总结——可变参数模板就是模板中的模板1.3库里面的可变参数模板——emplace系列接口1.3.1C11新增emplace系列接口1.3.1.1这里的“更高效”到底是怎么回事1.3.1.2push_back与emplace_back调用上的异同1.4在模拟list中看emplace_back的运行过程二新的类功能2.1默认的移动构造和移动赋值2.2成员变量声明时给缺省值2.3 defult和delete2.4 final与override三STL中一些变化概要序論这里是此方好久不见。本专栏是【主题曲C程序设计】专栏的补充篇【插曲现代C】。本系列将优先深度解析C11标准力求内容详实无微不至。C14~C20的进阶内容将在后续间隔一段时间后连载。本期将重点讲解可变参数模板、C11新的类功能以及STL中的新变化.好的让我们现在开始吧。C模板相关内容往期回顾本篇某种意义上也算是模版的现代进阶篇了doge模板初阶Re从零开始的 C 入門篇十二过渡章节模板初阶与STL简单介绍模板进阶Re从零开始的 C 进阶篇一超全的模板进阶详解非类型模板参数、模板特化、与模板的分离编译一模板中的模板可变参数模板1.1从概念开始——什么是可变参数与参数包1.1.1从C库中的printf引入在梦开始的地方C语言时期我们接触过一个函数printf他就是一个可变参数的函数。可变参数意味着我们可以传递任意多个参数给函数并完成相关操作。但是这个函数的可变参数的底层是通过一个数组来存储参数并打印的。我们接下来学习的模板的可变参数与此又有不同。1.1.2模板的可变参数C11支持可变参数模板也就是说支持可变数量参数的函数模板和类模板可变数目的参数被称为参数包存在两种参数包模板参数包表示零或多个模板参数函数参数包表示零或多个函数参数。templateclass...ArgsvoidFunc(Args...args){}templateclass...ArgsvoidFunc(Args...args){}templateclass...ArgsvoidFunc(Args...args){}我们用省略号来指出一个模板参数或函数参数表示一个包在模板参数列表中class… 或 typename… 指出接下来的参数表示零或多个类型列表在函数参数列表中类型名后面跟 … 指出接下来表示零或多个形参对象列表函数参数包可以用左值引用或右值引用表示跟前面普通模板一样每个参数实例化时遵循引用折叠规则。可变参数模板的原理跟模板类似本质还是去实例化对应类型和个数的多个函数。Tips新操作符追加“sizeof…”sizeof… 是 C11 引入的一个预处理器/编译时操作符返回变长参数模板中**“参数包”所包含的参数个数。**#includeiostream#includestringusingnamespacestd;templateclass...Argsvoidprint(Args...args){coutsizeof...(args)endl;}intmain(){inti0;intj1;doublek2;charl3;string m123456;print(i);print(i,j);print(i,j,k,l,m,1,9,546);return0;}如上图我们写了一个可变模板参数右值引用版本的函数print我们可以传递任意类型任意数量不分左右值的参数给函数print。函数内部的sizeof…操作符表达式语句打印结果分别是128.1.2简化代码的利器——可变参数模板该怎么用虽然我们通过 sizeof… 拿到了参数包的数量但仅仅知道“有多少个”是不够的。在实际开发中我们最迫切的需求是如何取出参数包里的每一个参数。与普通数组不同你不能通过 args[i] 这种下标方式来访问参数包。在 C11 中展开参数包主流的方式是编译递归法。1.2.1 递归函数方式展开参数包包扩展这是最经典、最符合“模板直觉”的方法。它的核心思想是将参数包拆解为第一个参数Head 剩余参数包Tail。通过不断递归调用自身一层层“剥开”参数包。#includeiostream#includestringusingnamespacestd;// 1. 递归终止函数,当参数包 args... 为空时会匹配这个无参的 ShowListvoidShowList(){coutendl;}// 2. 展开函数递归推导,每次调用都会将参数包的第一个参数赋给 x剩下的 N-1 个参数包给 args...templateclassT,class...ArgsvoidShowList(T x,Args...args){coutx ;ShowList(args...);}// 3. 可变参数模板入口templateclass...ArgsvoidPrint(Args...args){ShowList(args...);}intmain(){Print(1,string(xxxxx),2.2);return0;}就代码进行原理分析匹配 Print(int, string, double)它内部调用 ShowList(1, “xxxxx”, 2.2)。匹配 ShowList(T x, Args… args)此时 x 是 1剩下的包是 (“xxxxx”, 2.2)。编译器实例化出一个处理 int 的函数。递归调用 ShowList(“xxxxx”, 2.2)此时 x 是 “xxxxx”剩下的包是 (2.2)。编译器实例化出一个处理 string 的函数。递归调用 ShowList(2.2)此时 x 是 2.2剩下的包为空。编译器实例化出一个处理 double 的函数。递归调用 ShowList()匹配到那个最简单的无参终止函数递归结束。1.2.2包扩展的终止函数的设计为何重要有人看到我上面的这段代码可能会问了”哎呀此方啊递归调用在函数内部写一个递归终止条件不就好了怎么要这么麻烦“不对这和以前的递归还不一样上面模板的递归实际上是一个编译时递归实例化的过程。如下图在编译时通过不断的递归生成对应参数个数的函数模板。再通过参数类型将这些模板实例化成对应参数类型的函数。在运行时沿着如下的调用顺序依次调用这些已经被实例化出来的函数。于是回答上面的问题终止函数void ShowList(){ cout endl;}就非常必要因为如果没有它作为递归的出口编译器在面对“剥离”后变为空的参数包时会由于找不到匹配的函数原型而直接罢工报错。1.2.3强化对参数包的理解——另外一种调用场景分析如下代码Arguments(GetArg(args)…);要传递一个参数包给Arguments函数。这个调用Arguments函数参数包的每一个参数都是GetArg函数的调用返回值同时GetArg函数每次只能接收一个参数也就是说GetArg函数被调用了sizeof…(args)次每一次按顺序接收一个来自参数包… args的参数。在这个过程中编译器底层实际上也是帮助你生成了很多的函数并调用的就像这样Arguments(GetArg(1), GetArg(2), GetArg(3)…);templateclassTintGetArg(constTx){coutx ;return0;}templateclass...ArgsvoidArguments(Args...args){}templateclass...ArgsvoidPrint(Args...args){// 注意GetArg必须返回或者到的对象这样才能组成参数包给Arguments.Arguments(GetArg(args)...);}1.2.4总结——可变参数模板就是模板中的模板综上可变参数模板本质上是一个更加灵活的模板。如果不支持可变模板参数那么我也需要写五个这样的函数。就是说这个就是一个模板的模板。如下图对上文的包展开进行总结。需要注意的是这里的可变参数模板在编译器的实例化有两步编译器可能会合二为一成一步。实际上通过类似于这样的递归生成函数不能说是“递归调用”而应该是函数重载只能说编译时是递归编译生成运行时本质是函数重载我还想讲两句可能有小伙伴说C这种设计太挫了为什么参数包展开不设计成“底层用一个容器来存放参数遍历展开”呢实际上不是不想而是不能C17之前都不允许往一共容器里面放不同类型的参数。1.3库里面的可变参数模板——emplace系列接口1.3.1C11新增emplace系列接口templateclass...Argsvoidemplace_back(Args...args);templateclass...Argsiteratoremplace(const_iterator position,Args...args);C11以后STL容器新增了emplace系列的接口emplace系列的接口均为模板可变参数功能上兼容push和insert系列但是emplace还支持新玩法假设容器为container T emplace还支持直接插入构造T对象的参数这样有些场景会更高效一些可以直接在容器空间上构造T对象。emplace_back总体而言是更高效推荐以后使用emplace系列替代insert和push系列1.3.1.1这里的“更高效”到底是怎么回事很多小伙伴看完上文说emplace_back 比 push_back 快但总觉得云里雾里的。咱们直接看图里的核心逻辑其实就是“少了一次搬运”。push_back看图中左上角push_back 的参数类型是固定的 value_type这里咱们显式实例化成了 string。当你传一个字符串字面量 “1111111111” 进去时编译器发现类型不匹配一个是 const char*一个是 string。于是编译器会默默地先用这个字面量构造一个 string 临时对象。然后push_back 再把这个临时对象拷贝构造或移动构造到 list 的新节点里。代价 产生了一个临时对象并多了一次构造调用。emplace_back再看左下角emplace_back 是个可变参数模板。它的参数类型是在调用那一刻才确定的。当你传 “1111111111” 时它直接把这个 const char* 类型的参数原封不动地“传递”给了 list node 的构造函数。最终在容器申请好的那块内存空间上直接原地调用构造函数生成 string。代价 没有中间商赚差价直接在目的地合体.1.3.1.2push_back与emplace_back调用上的异同1.和push_back一样左值调用构造右值调用拷贝构造。intmain(){listpairbit::string,intlt1;// 跟push_back一样// 构造pair 拷贝/移动构造pair到list的节点中pairbit::string,intkv(苹果,1);lt1.emplace_back(kv);cout**********************endl;// 跟push_back一样lt1.emplace_back(move(kv));cout**********************endl;return0;}上面代码的测试结果string(char*str)-构造string(conststrings)--拷贝构造**************************string(strings)--移动构造**************************2.和push_back不同传参加不加{}有规矩。push_back的参数个数和类型已经被固定了必须传递一个{}扩起来的值。emplace_back不能传递一个{}括起来的值因为 { “苹果”, 1 } 这种初始化列表braced-init-list本身没有确定的类型它无法直接推导出模板参数 Args。——是的emplace_back不支持列表构造intmain(){listpairbit::string,intlt1;//这里达到的效果是push_back做不到的lt1.emplace_back(苹果,1);//lt1.push_back(苹果, 1); // ← 编译错误被注释或报错lt1.push_back({苹果,1});// 反而emplace不能这么写// lt1.emplace_back({ 苹果, 1 }); // ← 错误写法被划掉或注释lt1.emplace_back(苹果,1);lt1.push_back({苹果,1});// ← 正确使用初始化列表构造 pairreturn0;}1.4在模拟list中看emplace_back的运行过程我们模拟实现了list的emplace_back接口这里把参数包不段往下传递最终在结点的构造中直接去匹配容器存储的数据类型T的构造所以达到了前面说的emplace支持直接插入构造T对象的参数这样有些场景会更高效一些可以直接在容器空间上构造T对象。传递参数包过程中如果是 Args… args 的参数包要用完美转发参数包方式如下std::forward(args)… 否则编译时包扩展后右值引用变量表达式就变成了左值。// Test.cpp#includeList.h#includeiostreamusingnamespacestd;intmain(){bit::listpairstring,intlt1;lt1.emplace_back(苹果,1);return0;}//List.h#pragmaoncenamespacebit{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){}// itSelfoperator(){_node_node-_next;return*this;}Selfoperator--(){_node_node-_prev;return*this;}Refoperator*(){return_node-_data;}booloperator!(constSelfit){return_node!it._node;}};templateclassTclasslist{public:typedefListNodeTNode;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 newnode curprev-_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;};}二新的类功能2.1默认的移动构造和移动赋值原来C类中有6个默认成员函数,默认成员函数就是我们不写编译器会生成一个默认的。C11 新增了两个默认成员函数移动构造函数和移动赋值运算符重载。构造函数、析构函数、拷贝构造函数拷贝赋值重载、取地址重载、const 取地址重载如果你没有自己实现移动构造函数且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。委员会认为这三者是绑定在一起的你写了其中一者就会去写其他几者 那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数:对于内置类型成员会执行逐成员按字节拷贝(浅拷贝)。并没有“移动语义”中的夺取资源。自定义类型成员则需要看这个成员是否实现移动构造如果实现了就调用移动构造没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值重载函数:对于内置类型成员会执行逐成员按字节拷贝。自定义类型成员则需要看这个成员是否实现移动赋值如果实现了就调用移动赋值没有实现就调用拷贝赋值。默认移动赋值跟上面移动构造完全类似如果你提供了移动构造或者移动赋值编译器不会自动提供拷贝构造和拷贝赋值。2.2成员变量声明时给缺省值成员变量声明时给缺省值是给初始化列表用的如果没有显示在初始化列表初始化就会在初始化列表用这个却绳子初始化这个我们在类和对象部分讲过了。精准投送在这篇文章的第3.5点讲得非常详细Re从零开始的 C 入門篇九类和对象·最终篇上缓冲区同步与流绑定、取地址运算符重载、const成员函数、初始化列表2.3 defult和deleteC11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数但是因为一些原因这个函数没有默认生成。比如我们提供了拷贝构造就不会生成移动构造了那么我们可以使用 default 关键字显示指定移动构造生成。classMyClass{public:// 强制编译器生成默认构造函数MyClass()default;// 因为写了这个编译器原本不会自动生成 MyClass()MyClass(intx):_val(x){}private:int_val;};MyClass obj;// 有了 default这里才不会报错如果能想要限制某些默认函数的生成在C98中是该函数设置成 private并且只声明补丁已应为“只声明不定义”这样只要其他人想要调用就会报错。在C11中更简单只需在该函数声明加上 delete 即可该语法指示编译器不生成对应函数的默认版本称 delete 修饰的函数为删除函数。classNoCopy{public:NoCopy()default;// 禁止拷贝构造和赋值NoCopy(constNoCopy)delete;NoCopyoperator(constNoCopy)delete;};NoCopy a;// NoCopy b a; // 编译直接报错库里面的举例IO流不允许拷贝。2.4 final与override这个我们在继承和多态章节已经进行了详细讲过了精准投送这篇文章的第五点:Re从零开始的 C 进阶篇三彻底搞懂 C 多态虚函数、虚表与动态绑定的底层原理三STL中一些变化下图1圈起来的就是STL中的新容器但是实际最有用的是unordered_map 和 unordered_set。这两个我们前面已经进行了非常详细的讲解其他的大家了解一下即可。STL中容器的新接口也不少最重要的就是右值引用和移动语义相关的 push/insert/emplace 系列接口和移动构造和移动赋值还有 initializer_list 版本的构造等这些前面都讲过了还有一些无关痛痒的如 cbegin/cend 等需要时查文档即可。容器的范围 for 遍历这个在容器部分也讲过了。好了本期内容到此结束我是此方我们下期再见。バイバイ