php做网站知乎,境外网站icp备案,河北省网站备案管理系统,怎么清除网站文章目录 一、可变模板参数相关概念的引入二、获取参数包中参数的个数三、递归函数方式展开参数包四、逗号表达式展开参数包五、可变模板参数的实际应用——emplace相关接口5.1 回顾一下 push_back 的三种用法5.2 emplace_back 使用方法介绍5.3 听说 emplace_back 可以提高效率… 文章目录 一、可变模板参数相关概念的引入二、获取参数包中参数的个数三、递归函数方式展开参数包四、逗号表达式展开参数包五、可变模板参数的实际应用——emplace相关接口5.1 回顾一下 push_back 的三种用法5.2 emplace_back 使用方法介绍5.3 听说 emplace_back 可以提高效率 六、结语 一、可变模板参数相关概念的引入
C11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板相比C98/03类模板和函数模板中只能含固定数量的模板参数可变模板参数无疑是一个巨大的改进。然而由于可变模板参数比较抽象使用起来需要一定的技巧所以之一块还是比较晦涩的。本篇文章旨在帮助大家掌握一些基础的可变参数特性足够大家使用。
相信大家对可变参数这一概念并不陌生在 C语言阶段我们常用的 scanf 和 printf 它们就使用了可变参数但它们属于函数的可变参数和我们今天所要讲解的模板的可变参数有所不同。函数的参数传递的是对象而模板的参数传递的是类型非类型的模板参数除外函数的可变参数是希望传递任意个数的对象那模板的可变参数就是希望传递任意个数的类型。下面就是一个基本可变参数的函数模板。
templateclass ...Args
void ShowList(Args... args)
{}其中 Args 是一个模板参数包args 是一个函数形参参数包。声明一个参数包 Args... args这个参数包中可以包含 0 到任意个模板参数。参数 args 前面有省略号所以它就是一个可变模板参数我们把带省略号的参数称为“参数包”它里面包含了 0 到 N N0各模板参数。我们无法直接获取参数包 args 中的每个参数只能通过展开参数包的方式来获取参数包中的每个参数这是使用可变模板参数的一个主要特点也是最大的难点即如何展开可变模板参数。由于语法不支持使用 args[i] 这样的方式获取可变参数所以我们得用一些奇招来一一获取参数包的值。
二、获取参数包中参数的个数
templateclass ...Args
void ShowList(Args... args)
{cout sizeof...(args) endl; // 查看参数包中的参数个数
}int main()
{ShowList(1);ShowList(1, 1.1);ShowList(1, 1.1, a);return 0;
}可以通过 sizeof...(args) 来查看参数包中的参数个数。
三、递归函数方式展开参数包
//递归终止函数
templateclass T
void ShowList(T val)
{cout val endl;
}
// 可变模板参数
templateclass T, class ...Args
void ShowList(T val, Args... args)
{cout val ;ShowList(args...);
}int main()
{ShowList(1);ShowList(1, 2.1);ShowList(1, 2.1, a);return 0;
}该方法是通过递归调用 ShowList 函数去获取参数包中的参数每递归一次就可以从参数包中取出一个参数存到形参 val 中。注意采用这种方法获取参数包中的参数必须要重载一个仅有一个参数的同名函数也就是递归终止函数。假如不写这个函数会出现什么问题呢问题出现在当参数包中只有一个参数的时候如果参数包中只剩一个参数此时执行 ShowList(args...); 可以调用 void ShowList(T val, Args... args) 没有任何问题将参数包中仅存的一个参数传给第一个形参 val此时形参 args 参数包中没有任何东西然后再去递归调用 ShowList(args...); 这时问题就来了因为此时的 args 中什么都没有所以就相当于无参调用 ShowList();但是我们并没有重载 ShoeList 同名的无参函数所以就会报错。当我们写了上面的递归终止函数就不会出现这样的问题因为上面的递归终止函数中只有一个形参因此当参数包中只剩一个参数的时候 ShowList(args...); 会去走最匹配的也就是去调用我们写的递归终止函数这样就可以把参数包中的最后一个参数提取出来并且结束掉递归。通过上面的分析我们可以得出递归终止函数也可以重载成一个无参的同名函数像下面这样
// 递归终止函数
void ShowList()
{cout endl;
}
// 可变模板参数
templateclass T, class ...Args
void ShowList(T val, Args... args)
{cout val ;ShowList(args...);
}int main()
{ShowList(1);ShowList(1, 2.1);ShowList(1, 2.1, a);return 0;
}四、逗号表达式展开参数包
templateclass T
void PrintArg(T t)
{cout t ;
}// 可变模板参数
templateclass ...Args
void ShowList(Args... args)
{int arr[] { (PrintArg(args), 0)... };cout endl;
}int main()
{ShowList(1);ShowList(1, 2.1);ShowList(1, 2.1, a);return 0;
}这种展开参数包的方式不需要通过递归终止函数是直接在 ShowList 函数体中展开的PrintArg 不是递归终止函数只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。逗号表达式会按顺序执行逗号前面的表达式。ShowList 函数中的逗号表达式(PrintArg(args), 0)也是按照这个执行顺序先执行 PrintArg(args)再得到逗号表达式的结果0。同时还用到了 C11 的另外一个特性——列表初始化通过初始化列表来初始化一个边长数组{(PrintArg(args), 0)...}将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc...}最终会创建一个元素都为0的数组 int arr[sizeof...(args)]。由于是逗号表达式在创建数组的过程中会先执行逗号表达式前面的部分 (PrintArg(args) 打印出参数也就是说在构造 int 数组的过程中就将参数包展开了这个数组的目的纯粹是为了在数组构造的过程中展开参数包。
五、可变模板参数的实际应用——emplace相关接口
5.1 回顾一下 push_back 的三种用法
下面我们将采用 list 容器去探究 push_back 和 emplace_back 的用法与差异。list 中存的是 pairint, char 类型的对象。
定义一个存储 pairint, char 类型对象的链表
std::list std::pairint, char mylist;方式一
std::pairint, char pa(1, a);
mylist.push_back(pa);这种方式是最初阶的玩法先定义一个 pairint, char 类型的对象 pa此时会调用 pair 的构造函数。然后再将对象 pa 插入链表中。因为 pa 是一个左值所以最终会调用左值引用版本的插入即调用void push_back (const value_type val); 方式二
mylist.push_back(wcy::make_pair(2, b));
mylist.push_back(wcy::pairint, char(3, c));方式二是先调用 make_pair 函数创建一个 pairint, char 类型的对象然后直接将 make_pair 函数的返回值插入到链表中因为函数的返回值会被当做右值所以这里最终会去调用右值引用版本的插入。即void push_back (value_type val);。直接创建匿名对象进行插入的函数调用链和使用 make_pair 函数进行插入的函数调用链是一样的因为匿名对象的生命周期就只有一行编译器会把它识别成右值中的将亡值因此把这两种方式归为一类。
方式三
mylist.push_back({ 4, d });方式三的插入方式是 C11 新增的{4, d} 会去调用 pair 的列表初始化创建出一个 pairint, char 类型的对象。列表初始化本质上是 C11 允许多参数的构造函数支持隐式类型的转化。使用列表初始化创建出来的对象生命周期也只有一行会被编译器识别成右值因此最终回去调用右值版本的插入其函数调用链和方式二是一样的。
5.2 emplace_back 使用方法介绍
上面介绍的是 push_back 的使用方法下面来介绍 emplace_back 的使用方法。
template class... Args
void emplace_back (Args... args);emplace_back 与 push_back 最大的不同就在于它的参数采用了可变模板参数这就决定了它可以接受各种类型的参数而 push_back 的参数类型是固定的只能是 pairint, char 类型即链表中要存储的数据类型这在链表创建的初期就已经被确定下来了。由于 emplace_back 采用的是可变模板参数因此 push_back 的三种使用方式也同样适用于 emplace_back 这里就不再过多赘述这里主要想给大家分享一下 emplace_back 新增的一种使用方法。
mylist.emplace_back(5, e);要想搞懂 emplace——back 的原理我们需要先理解下面这段代码
class Date
{friend std::ostream operator(std::ostream out, const Date* date);
public:Date(int year 1900, int month 1, int day 1):_year(year),_month(month),_day(day){}private:int _year;int _month;int _day;
};std::ostream operator(std::ostream out, const Date* date)
{out date-_year 年 date-_month 月 date-_day 日 endl;return out;
}templateclass...Args
Date* CreatDate(Args...args)
{Date* date new Date(args...);return date;
}int main()
{Date* p1 CreatDate();Date* p2 CreatDate(2023);Date* p3 CreatDate(2023, 12);Date* p4 CreatDate(2023, 12, 30);Date* p5 CreatDate(*p3);// 最终是去调用拷贝构造cout p1 p2 p3 p4 p5;return 0;
}上面代码可以分为三个部分日期类、CreatDate函数、主函数。这里创建日期类对象是通过 CreatDate 函数来实现的。该函数使用了可变模板参数这样我们在主函数中调用 CreatDate 函数时可以传递任意个数的参数来创建 Date 类对象。new Date(args...) 最终是通过参数包的类型去调用构造函数或者拷贝构造函数。
mylist.emplace_back(5, e); 的原理和上面的逻辑是一致的就是将 (5, e) 放在一个参数包里一层层的往下传递最终还是去调用 pair 的普通构造函数。 5.3 听说 emplace_back 可以提高效率
首先说明所有的提高效率一般都是针对需要深拷贝的对象来说的提高效率就是减少深拷贝的次数。因上面的实验在 list 中存的是 pairint, char 类型对象这里不涉及深拷贝因此无法证明 emplace_back 可以提高效率。因此这里我们对 list 存储的对象类型进行修改让它存储一个需要进行深拷贝的对象 即 pairint, string 类型的对象。
int main()
{// 下面我们试一下带有拷贝构造和移动构造的bit::string再试试呢// 我们会发现其实差别也不到emplace_back是直接构造了push_back// 是先构造再移动构造其实也还好。std::list std::pairint, wcy::string mylist;mylist.emplace_back(10, sort);cout endl;mylist.emplace_back(std::make_pair(20, sort));cout endl;mylist.push_back(std::make_pair(30, sort));cout endl;mylist.push_back({ 40, sort });return 0;
}通过上面这段代码的执行结果可以看出使用 emplace_bakce 进行插入的时候对于需要深拷贝的对象它会将参数包一层层的往下传最终只调用一次普通的构造函数。而使用 push_back 进行插入的时候会先执行一次普通构造再调用一次移动构造。push_bakc 过程中调用普通构造是因为push_back 函数的参数在链表创建后就是固定的以上面的代码为例它的 push_back 函数的参数一定是 pairint, wcy::string 类型的对象引用可以是左值引用也可以是右值引用。因此首先需要创建一个 pairint, string 类型的对象作为实参。其中std::make_pair(30, sort) 和 { 40, sort } 就是去调用普通的构造函数创建对象作为实参通过这两条语句创建的对象叫做临时对象因为它的生命周期就只有一行所以这两条语句创建出来的对象会被编译器识别成右值最终去调用右值版本的插入。在右值版本的插入过程中会执行 new Node(forwardT(val)) 去创建节点移动构造就是在创建节点的时候去调用的。emplace_back 可以看作只在创建节点的时候调用了一次构造函数。通过前面的分析可以看出其实 emplace_back 并没有提高多少效率因为 push_back 使用移动构造的代价已经足够低了。移动构造中就是进行资源的置换一般就是指针的交换代价并不是很大。
小Tips总结一下对于需要进行深拷贝的对象来说emplace_back 和 push_back 的差距并不大。但是对于一个非常非常大的需要浅拷贝的对象来说因为浅拷贝的对象一般都不会自己去写拷贝构造和移动构造而是直接使用编译器默认生成的这种情况下编译器默认生成的都是完成浅拷贝那使用 push_back 会先调用一次构造再调用一次拷贝构造前后创建了两个大对象而 emplace_back 只会调用一次构造只创建一个大对象。需要注意前面说的这些都是建立在按照方式二或者方式三的方法或者使用 emplace_back 特有的方法去进行插入。
六、结语
今天的分享到这里就结束啦如果觉得文章还不错的话可以三连支持一下春人的主页还有很多有趣的文章欢迎小伙伴们前去点评您的支持就是春人前进的动力