可变模板参数(Variadic Templates)
可变模板参数
在学习模板编程时居然发现有类似于 “$…$” 的参数出现,它的作用在于代表输入的参数个数可能是 $0$ ~ $N$ ,也就是对参数进行了高度泛化。
可变函数模板展开
对于一个可变函数模板定义:
template<typename... T>
void func(T... args);
$…$的含义有二:
- 声明一个参数包 T… args ,这个参数包中可以包含0到任意个模板参数。
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
对于其的一个简单应用:
template<typename... T>
void func(T... args)
{
std::cout << sizeof...(args) << std::endl; //打印参数个数
}
func(); //0
func(1,2.5,"new"); //3
但是我们在应用中无法直接获取参数包 args 中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数。
可变模板参数在C++17之前一般有两种展开方式:递归展开和逗号表达式展开。
递归函数展开
对于一个递归,我们需要注意的第一个点就是确定递归终点。
#include <iostream>
void print()
{
std::cout << "end" << std::endl;
}
template<typename First, typename ...Args>
void print(First head, Args... rest)
{
std::cout << "it is " << head << std::endl;
print(rest...);
}
int main() {
print(1, 2.5, "newest");
}
output:
it is 1
it is 2.5
it is newest
end
如何理解这个递归呢?
其实相当于每次将这个参数包的首个元素取出并输出:
print(1,2.5,"newest");
print(2.5,"newest");
print("newest");
print();
以下是一个利用可变参数列表以及递归解包求和的例子:
#include <iostream>
template<typename T>
T sum(T t)
{
return t;
}
template<typename First, typename ...Args>
First sum(First head, Args... rest)
{
return head + sum(rest...);
}
int main() {
std::cout<<sum(1, 2, 3, 4, 5); //out:15
}
逗号表达式展开参数包
我们知道,对于逗号运算符,C++会从左到右计算表达式的内容,最终返回最右边的表达式计算的值。那么运用这个性质,运用一个边长数组的初始化即可展开参数包:
{(func(args), 0) ...}
将会被展开为:
(
(func(args0), 0),
(func(args1), 0),
(func(args2), 0),
...
)
最终在数组的初始化过程中会得到一个由0组成的数组。
#include <iostream>
template<typename T>
void printing(T t)
{
std::cout << t << std::endl;
}
template<typename ...Args>
void expand(Args... args)
{
int a[] = { (printing(args),0)... };
}
int main() {
expand(1, 1.8, "newest");
}
output:
1
1.8
newest
此时如果结合lambda表达式,我们还可以简化书写printing函数:
template<class F,typename ...Args>
void expand(const F& f, Args... args)
{
std::initializer_list<int>{
(
f(std::forward<Args>(args)),0
)...
};
}
int main() {
expand(
[](int i) {std::cout << i << std::endl; },
1, 2, 3
);
}
output:
1
2
3
当然上述写法仍然有一些限制,因为如此书写必须指定 $i$ 的类型,意味着参数包的类型必须是统一的,这个问题在C++14得到了解决,在C++14中允许在lambda表达式使用自动推导类型:
expand(
[](auto i) {std::cout << i << std::endl; },
1, 2, 3, "newest"
);
output:
1
2
3
newest
幸运的是,在C++17以后增加了折叠表达式
上述expand函数模板就可以进行简写了:
// 使用折叠表达式实现的版本
template<class F, class... Args>
void expand_fold(const F& f, Args&&... args)
{
// 使用逗号运算符的一元右折叠
(f(std::forward<Args>(args)), ...);
}
折叠表达式的工作原理
折叠表达式允许我们对参数包应用二元运算符。C++17 支持四种形式的折叠表达式:
- 一元右折叠: (pack op …)
- 一元左折叠: (… op pack)
- 二元右折叠: (pack op … op init)
- 二元左折叠: (init op … op pack)
其中:
pack 是包含参数包的表达式
| op 是二元运算符(如 +, -, *, /, &&, | , , 等) |
init 是初始值
以下是一些应用:
#include <iostream>
// 求和
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 一元右折叠
}
// 打印所有参数
template<typename... Args>
void print_all(Args... args) {
((std::cout << args << " "), ...); // 一元右折叠与逗号运算符
std::cout << std::endl;
}
// 检查是否所有参数都为真
template<typename... Args>
bool all(Args... args) {
return (args && ...); // 一元右折叠
}
// 带初始值的求和
template<typename... Args>
auto sum_with_init(int init, Args... args) {
return (init + ... + args); // 二元右折叠
}
int main() {
std::cout << "Sum: " << sum(1, 2, 3, 4, 5) << std::endl;
print_all("Hello", 42, 3.14, "World");
std::cout << "All true? " << all(true, true, true) << std::endl;
std::cout << "All true? " << all(true, false, true) << std::endl;
std::cout << "Sum with init: " << sum_with_init(10, 1, 2, 3, 4, 5) << std::endl;
}
output:
Sum: 15
Hello 42 3.14 World
All true? 1
All true? 0
Sum with init: 25
可变参数模板类
之前我们介绍的是可变参数模板函数的展开方式,现在我们介绍可变模板参数类。
在C++11中,引入了 $tuple$ 也就是元组类型,它实际上就是一个可变参数模板类,它的声明如下:
template <class _This, class... _Rest>
class tuple<_This, _Rest...> : private tuple<_Rest...> { // recursive tuple definition
...
}
此时我们可以如此创建一个 tuple 对象:
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, "");
当然,我们也可以建立一个空对象,但实际上它不是由上述声明所生成,而是一个特化版本:
emplate <>
class tuple<> { // empty tuple
public:
constexpr tuple() noexcept = default; /* strengthened */
constexpr tuple(const tuple&) noexcept /* strengthened */ {} // TRANSITION, ABI: should be defaulted
...
}
std::tuple<> tp;
递归展开可变参数模板类
接下来是一个用递归展开可变参数模板类的例子:
#include <iostream>
template <class ...Args>
class Sum;
template<class First, class ...Rest>
class Sum<First, Rest...>
{
public:
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};
template<class Last>
class Sum<Last>
{
public:
enum { value = sizeof(Last) };
};
int main() {
std::cout << Sum<int, double,std::string>::value<<std::endl;
}
当然我们可以通过声明时也加一个First 使得我们可以设置限制参数个数必须 $\gt 0$ :
template <class ...Args>
class Sum;
template<class First, class ...Rest>
class Sum<First, Rest...>
{
public:
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};
template<class Last>
class Sum<Last>
{
public:
enum { value = sizeof(Last) };
};
或者:
template<class First, class ...Rest>
class Sum
{
public:
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};
template<class Last>
class Sum<Last>
{
public:
enum { value = sizeof(Last) };
};
也可以在参数个数为0时终止:
template<>
class Sum<> {
public:
enum {value = 0;}
};
也可以使用std::integral_constant 去除enum定义展开参数包:
//前向声明
template<class First, class... Args>
class Sum;
//基本定义
template<class First, class... Rest>
class Sum<First, Rest...> :public std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};
//递归终止
template<class Last>
class Sum<Last> :public std::integral_constant<int, sizeof(Last)>
{
};
附std::integral_constant 源码:
template<class T, T v>
struct integral_constant
{
static constexpr T value = v;
using value_type = T;
using type = integral_constant; // using injected-class-name
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; } // since c++14
};