不要做一个清醒的堕落者
文章目录
- 可变参数模板的简介
- 什么是可变参数
- 模板参数包
- 参数包数据的获取(函数递归获取)
- 参数包的获取(逗号表达式获取)
- 可变参数的应用emplace
可变参数模板的简介
c++11添加的新特性能够让你创建可以接受改变的函数模板和类模板,C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
什么是可变参数
首先我们先来介绍一下什么是可变参数,我们先从函数说起吧,我们想想可变是什么意思,我们在写一个函数的时候我们的函数形参一般数量都是固定的,比如Add函数,max函数,min函数都是只能传递两个参数,那么有哪些函数可以传递的形参数目是不固定的呢?那当然是printf scanf,这两个函数的参数,数量都是变化的,并且参数类型也是,那么这是如何做到的呢?其实就是使用了可变参数模板,那么printf和scanf的底层其实使用了一个类似于数组的一个东西,而惊天我们讲述的则是更为高级些的类的可变参数。
模板参数包
首先我们先说一下格式
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
这里呢就是它的一个格式。
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
参数包数据的获取(函数递归获取)
那么我们可以通过什么方式呢?这里其实比较好的办法是通过函数递归进行展开。
代码如下
#include<iostream>
using namespace std;
template<class T>
void func(T val)
{
cout << val << " ";
}
template<class T,class ...Args>
void func(T val, Args... args)
{
cout << val<<" ";
func(args...);
}
int main()
{
func(1, 2, 3, 4, 5);
return 0;
}
这里的递归展开图呢我给大家画一下
那么这里呢就是这个函数的递归展开图其实这个args大家如果不理解可以将它看成是一个背包,这个背包可以是空的,然后可以把数据放进去,然后利用函数重载,将其一个一个取出来。其次呢我们就是只有一个参数的拿个函数我们称之为终止函数,因为有了这个函数才使得当这个背包里只剩下一个数据的时候才能有函数可调用,表示终止了。当然了也不止这一个方法。
参数包的获取(逗号表达式获取)
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
这里给大家说明一下,这里其实数组后面的三个点是交给编译器去推的,也就是说编译器眼里这里是怎们处理了呢?
int arr[]={(PrintArg(args), 0),(PrintArg(args), 0),(PrintArg(args), 0)}//是这个样子的
那么让我们自己去写肯定是不行的太麻烦所以交给编译器去推导,当然了我认为还可以更加简化一些简化为下面这个代码更好
#include<iostream>
#include<string>
using namespace std;
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
首先我们要理解为什么原来的代码要加逗号表达式里面写个0其实是因为数组不能为空,所以写的,那么我们直接将函数的返回值改为返回0不是也可以解决这个问题吗。
可变参数的应用emplace
emplace的函数声明如下:
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和
emplace系列接口的优势到底在哪里呢?
我们看一下图中的这个代码,我们发现emplace_back这样子插入没有报错而push_back这样却报错了,这是为什么呢?其实就是因为emplace_back的底层用了可变参数包。我们可以弄一个简易的类来给大家查看一下。
#include<list>
#include<string>
#include<iostream>
using namespace std;
namespace clzyf {
class data
{
private:
int year, month, day;
public:
data(int _year = 1, int _month = 1, int _day = 1)
:year(_year)
, month(_month)
, day(_day)
{
;
}
template<class... Arge>
void crate(Arge... arge)
{
data(arge);
}
};
}
int main()
{
clzyf::data a = { 2023,10,14 };
clzyf::data b={2023,10};
clzyf::data c={2023};
clzyf::data d={};
return 0;
}
请看这里我们通过传值将参数弄成一个参数包并且设置一下缺省参数这样子传递的话就可以弄出更多的花样,那么我们言归正传在上面的那个例子中其实也是这样的,他首先先将传递给emplace的参数弄成一个参数包然后将参数包打包给插入函数,因此就可以做到push_back无法做到的事情了。