C++11
的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比 C++98/03
,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
然而由于可变模板参数就比较抽象了,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。
下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个函数参数包args,这个参数包中可以包含0到任意个模板参数。
template<class... Args>
void func(Args... args)
{
// 获取可变参数个数
cout << "num= " << sizeof...(args) << endl;
}
int main() {
func(); // OK: args 不含有任何实参
func<int>(10); // OK: args 含有一个实参: int
func<int, char>(10, 'a'); // OK: args 含有两个实参 int 和 char
func<int, char, string, double>(10, 'a', "abc", 1.0); // OK: args 含有四个实参 int、char、string、double
return 0;
}
// 运行结果
num= 0
num= 1
num= 2
num= 4
上面的参数 args
前面有三个省略号,我们把 带省略号的参数称为“参数包”,它里面包含了 0
到 N
(N >= 0
)个模版参数。
另外在 sizeof…
是计算可变参数个数的。
现在我们无法直接获取参数包 args
中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。因为语法不支持使用 args[i]
这样方式获取可变参数,所以我们的用一些奇招来依次获取参数包的值。
通过递归方式展开参数包,需要提供一个参数包展开的函数和一个用于递归终止重载函数。 我们可以通过递归调用该函数来实现获取参数包的值,其中有几个要注意的点:
ShowList(args...)
。ShowList
,当我们的参数包调用剩下一个参数的时候,调用的是重载版本的 ShowList
进行递归的终止。ShowList
的重载版本要放在普通版本的声明上方,因为它们的模板参数不同,不是特化版本,只是重载,如果不在普通版本的上面的话,ShowList(args...)
调用到最后的一个参数包的值的时候,只剩下一个参数了,而普通版本是需要大于两个参数才能匹配的,所以会报错,所以要将特殊版本的 ShowList
放到上面,当参数等于一个的时候优先匹配特殊版本的!// 特殊版本的ShowList,是递归终止的函数
template <class T>
void ShowList(const T& val)
{
cout << typeid(val).name() << ":" << val << endl << endl;
}
template<class T, class ...Args>
void ShowList(T val, Args... args)
{
cout << typeid(val).name() << ":" << val << endl;
ShowList(args...); // 调用自己的时候,直接传自己的包过去成为新的val
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("liren"));
return 0;
}
//运行结果
int:1
int:1
char:A
int:1
char:A
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >:liren
其实也可以写成下面这样子,这种写法和上面的区别在于:上面的写法是等到参数包剩下一个参数的时候接收,而下面这种就是等参数包空了再接收,这个不同场景的应用都是不同的,具体情况具体分析!
// 特殊版本的ShowList,是递归终止的函数
void ShowList()
{
cout << "end" << endl << endl;
}
template<class T, class ...Args>
void ShowList(T val, Args... args)
{
cout << typeid(val).name() << ":" << val << endl;
ShowList(args...); // 调用自己的时候,直接传自己的包过去成为新的val
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("liren"));
return 0;
}
// 运行结果
int:1
end
int:1
char:A
end
int:1
char:A
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >:liren
end
下面这种展开参数包的方式,不需要通过递归终止函数,是直接在 ShowList
函数体中展开的,其中 PrintArg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
这种就地展开参数包的方式实现的关键是逗号表达式:逗号表达式会按顺序执行逗号前面的表达式,但是返回的是最后的那个表达式。
ShowList
函数中的逗号表达式:(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 val)
{
cout << typeid(val).name() << ":" << val << endl;
}
//展开函数
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:1
int:1
char:A
int:1
char:A
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >:sort
其实还有更简单的写法,不用这么复杂:
template <class T>
int PrintArg(T val)
{
cout << typeid(val).name() << ":" << val << endl;
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;
}
// 运行结果
int:1
int:1
char:A
int:1
char:A
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >:sort
这里将 PrintArg
的返回值变成了 int
,这样子的话我们就可以不用逗号表达式了,直接在数组中使用参数包即可!
STL
容器中的 emplace
接口函数 在之前学 vector
等容器源码剖析的过程中,经常发现这样的代码,如下:
template <class... Args>
void emplace_back (Args&&... args);
可以看到里面模板参数是 template<typename... _Args>
,其实这个就是变参数模板,然后它的参数也是比较特别的 _Args&&... __args
,去除右值引用的话,它就是一个简单的可变参数,那么可变参数模板和可变参数到底是什么,应该怎么使用呢,我们下面就来深究一下这些事情。
首先我们看到 emplace
系列的接口,支持模板的可变参数,并且使用万能引用。那么相对 insert
和 emplace
系列接口的优势到底在哪里呢?
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
// mylist.push_back(40, 'd'); // ❌push_back不支持可变参数
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
// 运行结果
10:a
20:b
30:c
40:d
50:e
下面我们试一下自己写的带有拷贝构造和移动构造的 string
:
int main()
{
// 我们会发现其实差别也不大
// emplace_back是直接构造了,push_back是先构造,再移动构造,其实也还好。
std::list< std::pair<int, liren::string> > mylist;
mylist.emplace_back(make_pair(10, "sort"));
mylist.emplace_back(20, "sort");
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort"});
return 0;
}
// 运行结果
string(char* str) -- 构造函数
string(char* str) -- 构造函数
string(char* str) -- 构造函数
string(string&& s) -- 移动构造
string(char* str) -- 构造函数
string(string&& s) -- 移动构造
emplace_back
是直接构造了,push_back
是先构造,再移动构造,其实也还好,因为有了移动构造,其实只是一个资源转移,也不会说有太多的消耗,因为基本都是指针类型的交换转移,所以几乎是没有什么区别的。
最大的区别就是 emplace_back
的写法更方便了,可以不用加 {}
就能用参数包去初始化即可~
可变参数模板类是一个带可变模板参数的模板类,比如 C++11
中的元组 std::tuple
就是一个可变模板类,它的定义如下:
template<class... Types>
class tuple;
这个可变参数模板类可以携带任意类型任意个数的模板参数:
std::tuple<> tp; // 可变参数模板的模板参数个数可以为0个,所以该定义也是合法的:
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, "");
可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开方式比可变参数模板函数要复杂,需要通过模板特化和继承方式去展开。下面我们来看一下展开可变模版参数类中的参数包的方法。
可变参数模板类的展开一般需要定义 2 ~ 3
个类, 包含类声明和特化的模板类,包含下面三种:
#include <iostream>
#include <typeinfo>
using namespace std;
template <typename... A> class BMW{}; // 可变参数模板声明
template <typename Head, typename... Tail> // 递归的偏特化定义,递归继承模板类
class BMW<Head, Tail...> : public BMW<Tail...>
{
public:
// 当实例化对象时,则会引起基类的递归构造
BMW()
{
printf("type: %s\n", typeid(Head).name());
}
private:
Head head;
};
template<>
class BMW<> {}; // 边界条件
int main()
{
BMW<int, char, float> car;
return 0;
}
// 运行结果
type: float
type: char
type: int
模板递归和特化方式展开可变参数模板类:
#include<iostream>
// 1、变长模板声明
template<int ... last>
class Test {};
// 2、变长模板类定义
template<int first, int ... last>
class Test<first, last...>
{
public:
static const int val = first * Test<last...>::val; // 递归调用模板类
};
// 3、边界条件
template<>
class Test<>
{
public:
static const int val = 1;
};
int main() {
int sum = Test<2, 3, 4, 5>::val;
std::cout << sum << std::endl;
return 0;
}
// 运行结果:
120