可变参数模板
可变参数模板(C++11)
上周末实现双向链表时参照了STL中的list链表,在查阅文档时发现list下的emplace方法的模板参数是template< class... Args >
当时就很疑惑,不知道这是什么参数,在网上学习了他人的博客后在此写下自己的见解。
可变参数的概念
可变参数即可以接收多个参数,如C语言中的printf函数
和scanf函数
其中的...
便是可变参数列表,使函数可以接受多个类型不同的参数printf("%f%d", 2.54, 't');
。
可变参数模板的概念
C++11引入了可变参数模板,将可变参数应用到模板上,使得模板可以接收多个参数.
声明一个可变参数模板:
template <typename... Args>
void function(Args... args);
其中省略号有两个含义:
typename... Args
表示声明模板参数包,该模板可接收多个类型不同的参数。Args... args
表示声明一个参数包,这个参数包中可以包含0到N(N>=0)个模板参数;
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取;而可变参数模板又可应用到函数和类中,并且模版函数不支持偏特化,所以可变参数模版函数和可变参数模版类的展开方法还不尽相同。
可变参数模板函数
使用sizeof...()
运算符可以计算参数包的大小:
template <typename ...Args> //模板参数包
void ShowSize(Args... args) //函数形参参数包
{
std::cout << sizeof...(args) << std::endl; //sizeof...运算符用来查询参数包中的元素个数
}
ShowSize(1, 'j', 4.0); //3
但这样并不能获得参数包中实际的参数,要获得实际的参数可以通过使用递归函数来展开参数包,也可以使用逗号表达式展开参数包。
递归函数展开参数包
通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归函数的终止条件便是递归终止数。
递归展开函数,每次递归时接收一个参数和剩余的参数包:
template <typename T, typename ...Args>
void ShowParameterPack(T value, Args... args)
{
std::cout << value << " "; //执行对参数的操作
ShowParameterPack(args...); //递归
}
有参递归终止函数需写成模板的形式,当参数包只剩最后一个参数时,匹配此函数从而终止递归,使用有参形式则不能传入空参数包:
template <typename T>
void ShowParameterPack(constT& value)
{
std::cout << value << std::endl; //执行对最后一个参数的操作
}
无参递归终止函数,当参数包没有参数时匹配此函数:
void ShowParameterPack()
{
std::cout << std::endl; //可不进行任何操作
}
逗号表达式展开参数包
使用递归展开必须要有一个终止函数,而使用逗号表达式加初始化列表则不需要则不需要。
逗号表达式展开函数,其中{ (PrintArgs(args), 0)... }
使用了初始化列表:
template <typename T>
void PrintArgs(const T& value) //对参数进行操作的函数
{
std::cout << value << " ";
}
template <typename ...Args>
void ShowParameterPack(Args... args) //展开函数
{
int arr[] = { (PrintArgs(args), 0)... };
std::cout << std::endl;
}
在执行此函数时,由于逗号表达式的特性,会先计算左边的表达式,最后返回最后一个表达式,也就是先执行PrintArgs(args)
,而初始化列表{}
会将表达式(PrintArgs(args), 0)...
展开,从而就可以在构造数组的过程中执行对各个参数的操作并返回0,最后形成一个全是0的数组。
但这样会无法处理参数包为空的情况,若想处理此情况,还需重载一个接收空参数的展开函数:
void ShowParameterPack()
{
std::cout << std::endl; //可不进行任何操作
}
其实也可以不使用逗号表达式,只需在展开函数时返回一个与数组匹配的值即可:
template<class T>
int PrintArg(const T& value)
{
std::cout << value << " ";
return 0;
}
template<class ...Args>
void ShowParameterPack(Args... args)
{
int arr[] = { PrintArg(args)... }; //没使用逗号表达式
std::cout << std::endl;
}
当然执行操作的函数也可使用lambda表达式来代替:
template<class F, class... Args>
void ShowParameterPack(const F& function, Args&&...args)
{
int arr[] = {function(args)...};
}
ShowParameterPack([](auto value) {std::cout << value << " "; return 0; } , 1, 'j', "CL");
其中使用了auto value
来接收各种类型不同的参数。
可变参数模板类
带可变参数模板的模板类便是可变参数模板类,如元祖std::tuple
就是一个可变参数模板类。
可变参数模板类可使用偏特化加递归和继承来展开参数包。
模板偏特化加递归展开参数包
使用模板偏特化展开首先要进行前向申明一个可变参数模板类:
//前向声明
template <typename... Args>
class Expend;
随后对模板类进行偏特化,使一个参数与参数包分离,并进行递归调用,使所有参数分离:
//参数分离递归展开参数包
template <typename T, typename... Args>
class Expend<T, Args...>
{
public:
Expend(T value, Args... args)
{
std::cout << value << std::endl;
Expend<Args...> e(args...); //递归调用
}
};
Expend<int, double, const char*, char> e(1, 2.5, "HeavenBurn", 'c');
最后再次对模板类进行偏特化以构成递归的终止条件,当参数包只剩最后一个参数时会调用:
//展开参数包的终止条件
template <typename T>
class Expend<T>
{
public:
Expend(T value)
{
std::cout << value << std::endl;
}
};
当然终止类也可以使用无参的形式,当参数包全部展开时会调用:
//展开参数包的终止条件
template <>
class Expend<>
{
public:
Expend() {}
};
也可以将前向声明与终止条件整合到一起,因为参数包可以接受0个参数,当参数包全部展开时就会调用原模板类:
template <typename... Args>
class Expend
{
public:
Expend(Args... args) {}
};
继承方式展开参数包
首先要定义一个基类:
template <typename... Args>
class Expend
{
public:
Expend(){}
};
随后定义具有展开功能的子类继承父类,Expend继承于原始模板类,自身为可将参数分离的特化模板类,在构造时将参数包传给父类,再次发起展开,当无参数时则通过父类终止展开:
template <typename T, typename... Args>
class Expend<T, Args...> : public Expend<Args...>
{
public:
Expend(T value, Args... args)
: Expend<Args...>(args...) //将扩展参数包传给父类
{
std::cout << value << std::endl;
}
};
Expend<int, double, std::string, char> e(1, 2.5, "HeavenBurn", 'c');
也可使用特化的模板类来终止展开,会优先调用:
template <>
class Expend<>
{
public:
Expend(){}
};
折叠表达式(C++17)
以二元运算符对形参包进行处理,省去使用递归,逗号表达式等方法展开参数包的步骤。折叠表达式中的运算符包含32个二元运算符:
+
-
*
/
%
^
&
|
=
<
>
<<
>>
+=
-=
*=
/=
%=
^=
&=
|=
<<=
>>=
==
!=
<=
>=
&&
||
,
.*
->*
。在二元折叠中,两个运算符必须相同。
语法
- 一元右折叠:将
(E 运算符 ...)
转换为(E 1 运算符 (... 运算符 (E N-1 运算符 E N)))
- 一元左折叠:将
(... 运算符 E)
转换为(((E 1 运算符 E 2) 运算符 ...) 运算符 E N)
- 二元右折叠: 将
(E 运算符 ... 运算符 I)
转换为(E 1 运算符 (... 运算符 (E N−1 运算符 (E N 运算符 I))))
- 二元左折叠:将
(I 运算符 ... 运算符 E)
转换为((((I 运算符 E 1) 运算符 E 2) 运算符 ...) 运算符 E N)
其中E表示参数包,I表示为不含参数包的表达式。
使用示例
- 使用参数包和折叠表达式打印:
template <typename... Args> void Print(const Args&... args) { (std::cout << ... << args) << "\n"; } Print(1, 3, 5, 97, 'a'); // 13597a