最近在看有关智能指针源码的时候make_unique,make_shared(用来创建管理一个新对象)模板都是这么定义的
template
std::unique_ptr make_unique(Ts&&... params)
{
return std::unique_ptr(new T(std::forward(params)...));
}
看的时候一脸懵逼,这...是什么,后来了解这种叫做可变参数模板,Ts&&... params是他的参数,去除右值引用的话,它就是一个可变参数Ts... params,因此就先学习了一下,写这篇文章也是简单记录一下,之后闲了也能看看,有问题和补充的欢迎大佬评论区留言!
一个可变参数模板通俗来讲就是:一个接受可变数目参数的模板函数或模板类
在c++11之前,类模板和函数模板只能含有固定数量的模板参数,c++11增加了可变模板参数特性:允许模板定义中包含0到任意个模板参数。声明可变参数模板时,需要在typename或class后面加上省略号"..."。 就像我上面指出的源码那样
在了解其之前我们要先知道什么是参数包?
可变数目的参数被称为参数包。
存在两种参数包:
模板参数包:表示零个或多个模板参数
函数参数包:表示零个或多个函数参数
怎么好理解可变参数模板呢?其实我是这么认为的,对于可变参数模板来说,相对于普通的模板,他其实就是多了个“...”关键字,“...”所在的位置不同,代表的含义也不一样。
先来笼统的说说省略号的作用,后面给出例子一一去解释一波:
先来说说他的语法格式哈
- 用一个省略号来指出一个模板参数或函数参数表示一个包
- 在模板参数列表中:class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个多多个给定类型的非类型参数的列表
- 在函数参数列表中:如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包
光说不练假把式,我们来一步步拆解这个语法,进行编写
如何去定义可变参数模板呢?其实他和普通模板差不多,就是多个省略号
//普通模板
template//只能有一个类型参数
//可变参数模板
template
对于可变参数模板来说...
可接纳的模板参数个数是0个及以上的任意数量,也就是说“... Types”可以接受0到任意的参数
当然如果不希望产生模板参数个数为0的变长参数模板,可以写成下面的形式,这种也是最常用的
template
因为多了个T类型参数,所以调用它的时候最少也得接纳一个参数,因为...Types是0到任意嘛
下面我给出一个简单的代码来看看他的实例化调用
模板参数列表中:声明一个名为T的类型参数,和一个名为Args的模板参数包(这个包表示零个或多个额外的类型参数)
函数参数列表中:声明一个const&类型的参数,指向T的类型,还包含一个名为reset的函数参数包(这个包表示零个或多个函数参数)//Args是一个模板参数包;rest是一个函数参数包 //Args表示零个或多个模板类型参数 //rest表示零个或多个函数参数 template
void fun(const T& t, const Args& ... reset) { //... } 我们给出基本调用:
int main() { int i = 1; double d = 3.30; string s = "happy birthday"; fun(i, s, 55, d); //包含三个参数 fun(s, 55, "hello"); //包含二个参数 fun(d, s); //包含一个参数 fun("hello"); //空包,传的其实是T类型的参数 fun(10); //空包,传的其实是T类型的参数 return 0; }
上面的五个fun()函数调用会实例化下面4个版本:
//第一个T的类型从第一个实参推断出来,剩余的实参从提供的参数包中推断出 //第一个参数都是T推演过来的,后面都是...Args推演过来的 void fun(const int&, const string&, const int&, const double&); void fun(const string&, const int&, const char[6]&); void fun(const double&, const string&); void fun(const char[6]&); void fun(const int &);
为什么模板定义的时候“...”在类型前,写函数参数列表的时候“...”在类型后了
前面我有说过“...”位置不同,代表的意思不同,下面我就一一说明~
void fun() {
//这里加一个函数是为了编译可以通过,否则编译期间调用就会找不到void fun(const T (&))可匹配的函数
}
template
void fun(const T& t, const Args& ... reset)
{
cout << t << endl;;
fun(reset...);
}
主函数代码还是上面的,运行结果显示:
对于这个几个“...”的位置我来解释一下:
(1)模板参数包
第一个typename...Args,写在template 的<>中,因此在使用模板函数时可以指定任意类型(在main函数中的fun调用)
(2)函数参数类型包(扩展包)
第二个Args ... reset,用在fun函数的形参中,表示任意类型的任意参数
(3)函数参数包
第三个reset...,用在fun函数的递归调用中,实际上是实参
什么意思呢?如果我去掉fun(reset...)这句话,不递归调用fun
就会出现下面结果:
(4)其他情况
若想要获得参数包中的参数数量,需要使用sizeof...(reset)来获得
PS:上面看到了...的使用情况:在形参左侧、参数右侧
1)省略号出现形参名字左侧,声明了一个参数包(parameter pack)。使用这个参数包,可以绑定0个或多个模板实参给这个可变模板形参参数包。参数包也可以用于非类型的模板参数(也就是参数数量不固定,但是每个参数的类型是一样的,即将Args reset替换成 int reset)。
fun(1,'a',1.1,"hi");
2)省略号出现包含参数包的表达式的右侧,则把这个参数包解开为一组实参(也就是reset... = reset1, reset2, reset3,...)。这也就是为什么递归要这么调用了
执行上面那句话,我们会发现他会不断的拆解递归调用,进程包拓展
递归到最后,再一步步返回
递归调用才能打印正确
如果不递归调用,只会打印第一个1和后面fun1(10)
因此在写可变参数模板的时候一定要加上他的递归调用
再来聊聊一些用法:
当我们想要知道包中有多少元素时,可以使用sizeof...运算符,该运算符返回一个常量表达式,并且不会对其实参求值
template
void g(Args... args)
{
std::cout << sizeof...(Args) << std::endl; //类型参数的数目
std::cout << sizeof...(args) << std::endl; //函数参数的数目
}
int main()
{
int i = 0;
double d = 3.14;
string s = "how now brown cow";
g(i, s, 42, d);
g(s, 42, "hi");
g(d, s);
g("hi");
g();
return 0;
}
会发现没有指定额外的参数模板,传入的参数都会被参数包接收
而对于指定了额外参数模板T之后,只传入一个参数,会被T类型接收,参数包为0
template
void g(const T &t,Args... args)
{
std::cout << sizeof...(Args) << std::endl;
std::cout << sizeof...(args) << std::endl;
}
int main()
{
int i = 0;
double d = 3.14;
string s = "how now brown cow";
g(i, s, 42, d);
g(s, 42, "hi");
g(d, s);
g("hi");
return 0;
}
对于可变参数模板递归调用我上面大概讲了一下,包拓展放了截图和递归一起演示说了,如果有点迷糊的戳这里主要看看包拓展和递归
上面我只讲了递归进行包展开,其实也可以直接把整个形参包展开以后传递给某个适合的函数或者类型。c11后stl引入emplace,emplace_front,back等等原位的构造,其源码也是利用这个模板去实现。了解可以戳这块
对于可变参数模板用在类上转到这个这个这个,元组tuple的实现就是基于可变参数类模板,tuple是pair对组的拓展,了解戳这里。