可变模板参数函数

可变参数模板的声明一般是template<class…>

Template<class... T>
void func(T... args);

args前面有省略号,我们把这种带省略号的参数称为”参数包”,可以包含0到任意个模板参数。
一个简单的例子:

template <class... T>
void F(T... args)
{
cout << sizeof...(args) << endl; //打印变参的个数
}

void test_param() {
F();
F(1, 2);
F(1, "", 2);
}

// result
0
2
3

递归方式展开可变参数

我们无法直接获取参数包args中的每一个参数,只能通过展开参数包的方式来获取每一个参数。这是可变模板参数的一个主要特点。
要想实现这种展开,我们需要实现2个函数:展开函数递归终止函数

//递归终止函数
void print()
{
cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
cout << "parameter " << head << endl;
print(rest...);
}

int main() {
print(1, 2, 3, 4);
return 0;
}

// result
parameter 1
parameter 2
parameter 3
parameter 4
empty

这个例子用来打印出每一个参数,包含展开函数和递归终止函数。rest…后面的省略号能把参数展开,首先我们可以把参数1匹配到head,剩下的2、3、4匹配到参数包rest。这里把参数1打印出来,然后参数包rest…展开成2、3、4。当参数展开完后,就调用递归终止函数,打印empty
再看一个简单的求和计算的例子:

#include <iostream>

template<class T>
T sum(const T& val) {
return val;
}

template<class T, class... Args>
T sum(const T& val, const Args&... args) {
return val + sum(args...);
}

int main() {
std::cout << sum(1, 2, 3) << std::endl;
return 0;
}

// result
6

当两个函数模板的区别只有尾部的可变参数包时,会优先选择没有参数包的那个函数模板。

逗号表达式+初始化列表展开参数包

首先我们通过代码看一下利用初始化列表展开参数包的效果。

template<class... Args>
void init(Args... args) {
int arr[] = {(args+100)...};
for(int i = 0; i < sizeof...(args); ++i){
std::cout << arr[i] << std::endl;
}
}

int main() {
init(1, 2, 3);
return 0;
}

// result
101
102
103

从上面可以看出在初始化列表中,省略号对args进行了展开,这里会将args展开成(args0+100)(args1+100)(args2+100)的形式。现在我们再加上逗号表达式来展开可变参数,重新实现上面的可变参数打印。

template <class T>
void printarg(T t)
{
cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), 0)...};
}

int main() {
expand(1,2,3);
return 0;
}
// result
1
2
3

(printarg(args), 0)这个表达式返回的值是0(逗号表达式的原因,需要给数组进行赋值),参数展开成 (printarg(args0), 0)(printarg(args1), 0)(printarg(args2,0))。最后arr变成了一个初始值全是0的数组。我们可以把上面的例子改进下,使用lambda表达式来实现,更加简洁。

template<class F, class... Args>
void nexpand(const F& f, Args&&...args)
{
initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}

int main() {
nexpand([](int val){
std::cout << val << std::endl;
}, 1, 2, 3);
return 0;
}

// result
1
2
3

c++17的折叠表达式

折叠表达式语法说明

c++17提供了一种折叠表达式的黑科技来展开可变参数。一共有四种形式:一元右折叠、一元左折叠;二元右折叠、二元左折叠。

  • 一元右折叠:($E$ op …)后变成了$E_{1}$ op (… op ($E_{n-1}$ op $E_{n}$ ))
  • 一元左折叠:(… op $E$)后变成了(($E_{1}$ op $E_{2}$) op …) op $E_{n}$
  • 二元右折叠:($E$ op … op $I$)后变成了$E_{1}$ op (… op ($E_{n-1}$ op ($E_{n}$ op $I$) ))
  • 二元左折叠:($I$ op … op $E$)后变成了((($I$ op $E_{1}$) op $E_{2}$) op …) op $E_{n}$

上面公式中的$E$表示参数包,$E_{i}$表示第$i$个参数,op表示操作符,$I$表示初始参数。

新的“打印”应用

直接看下面的样例,提供了两种实现,一种是直接在函数内部进行打印,但是打印方式受到限制,无法定制化打印。第二种传入一个打印函数,可以进行定制化打印。

template<class... Rest>
void print17(Rest&&... rest) {
(std::cout << ... << rest) << std::endl;
}

template<class Func, class... Rest>
void expand17(Func&& func, Rest&&... rest) {
(func(rest), ...);
}

print17(1, 2, 3);
expand17([](int a){
std::cout << a << " ";
}, 1, 2, 3);

// result
123
1 2 3

可变模板参数类

可变参数模板类是一个带可变模板参数的模板类,比如元组std::tuple就是一个可变参数模板类。

template<class... Args>
class tuple;

模板偏特化和递归方式展开参数包

可变模板参数类的展开方式比可变模板参数函数要复杂一些。需要对类进行声明,提供一个类的基本定义和一个偏特化的递归终止类。

//前向声明
template<typename... Args>
struct Sum;

// 偏特化 基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum { value = Sum<First>::value + Sum<Rest...>::value };
};

// 偏特化 递归终止
template<typename Last>
struct Sum<Last>
{
enum { value = sizeof (Last) };
};

int main() {
std::cout << Sum<int, double, int>::value << std::endl;
return 0;
}

// result
16

参考