模板编程中的可变参展开

可变参

1
2
template <typename ...Args>  
void func(Args ...args);

args 叫做函数参数包,里面包含传递的参数。

Args 叫做模板参数包,里面包含传递参数的类型。

说明:省略号写在参数包的左边,代表打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename ...Args>
void display(Args ...args) {
cout << "参数个数 " << sizeof...(args) << endl;
cout << "参数类型个数 " << sizeof...(Args) << endl;
}

int main() {
display("xy", 18, "Student", 65.5);
return 0;
}

/*
参数个数 4
参数类型个数 4
*/

光会这个没什么价值,我们肯定希望能够把每个变量都可以读取到。

需要对参数包进行解包(展开)。每次解出第一个参数,然后递归调用函数模板,直到递归出口

注:省略号写在参数包的右边,代表解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void display() {                // 当没有参数时,打印一个空行并终止递归
cout << "over" << endl;
}

template<typename T, typename... Args>
void display(T first, Args... args) {
cout << first << endl; // 处理第一个参数

display(args...); // 递归处理剩下的参数
}

int main() {

display("xy", 18, "Student", 65.5);

return 0;
}

display("xy", 18, "Student", 65.5) 被调用时,递归展开如下:

  • 第一次调用:first = "xy", args = 18, "Student", 65.5,打印 "xy",然后递归调用 display(18, "Student", 65.5)
  • 第二次调用:first = 18, args = "Student", 65.5,打印 18,然后递归调用 display("Student", 65.5)
  • 第三次调用:first = "Student", args = 65.5,打印 "Student",然后递归调用 display(65.5)
  • 第四次调用:first = 65.5, args = {}(空),打印 65.5,然后递归调用 display()
  • 最终调用:没有参数时,调用无参的 display(),打印 "over"

当我们想要获取全部的参数类型,重新定义一个可变参数模板,至少得有一个参数,因此我们在前面的代码基础上额外添加一个 typename T 。

1
2
3
4
5
6
7
8
9
最初:

template <typename ...Args>
void func(Args ...args);

为了展开:

template<typename T, typename... Args>
void display(T first, Args... args);

由于递归之后,必然会遇到没有参数,所以需要单独写一个没有参数的同名函数,用以结束。

那如果,我们希望一时获取两个参数,或者更多参数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void display() {                // 当没有参数时,打印一个空行并终止递归
cout << "over" << endl;
}

template<typename T, typename G,typename... Args>
void display(T first,G second, Args... args) {
cout << first << " " << second << endl; // 处理两个参数

display(args...); // 递归处理剩下的参数
}
int main() {

display("xy", 18, "Student");

return 0;
}

传递三个参数,但是我们每次都要展开两个参数,必然会剩下一个参数,但是我们并没有额外写一个同名函数接受单个参数,因此编译报错。解决方案见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void display() {                // 当没有参数时,打印一个空行并终止递归
cout << "over" << endl;
}

template<typename T>
void display(T arg) { // 处理单个参数
cout << arg << endl;
}

template<typename T, typename G,typename... Args>
void display(T first,G second, Args... args) {
cout << first << " " << second << endl; // 处理两个参数

display(args...); // 递归处理剩下的参数
}

 

递归的出口可以使用普通函数或者普通的函数模板,但是规范操作是使用普通函数

(1)尽量避免函数模板之间的重载;

(2)普通函数的优先级一定高于函数模板,更不容易出错。

 

如上是函数模板中的形参包展开,类模板中同样是如此的展开方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename... Args>
class Printer {
public:
Printer(Args... args) {
print(args...);
}

private:

void print() {
std::cout << "End of arguments\n";
}

template <typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first << " ";
print(rest...);
}
};

变量模板是 C++17 引入,而折叠表达式也是 C++17 引入,当我们还是在这里介绍 变量模板的展开:

1
2
3
4
5
6
7
8
template<std::size_t...values>
constexpr std::size_t array[]{ values... };

int main() {
for (const auto& i : array<1, 2, 3, 4, 5>) {
std::cout << i << ' ';
}
}

array 是一个数组,遍历这个数组进行展开。

折叠表达式

C++11 引入了可变参数模板(variadic template),它可以接收任意数量的模板参数,但是参数包不能直接展开,需要通过递归或者逗号表达式的方式进行展开,写法非常繁琐。C++17 对这个问题进行了优化,引入了折叠表达式的概念,用来简化对可变参数模板中参数包的展开过程。

1
2
3
4
5
6
7
8
(pack op ...) 						//一元 右 折叠

(... op pack) //一元 左 折叠

(pack op ... op init) //二元 右 折叠

(init op ... op pack) //二元 左 折叠

注:折叠表达式是左折叠还是右折叠,取决于 ... 是在“pack”的左边还是右边

op:折叠表达式支持如下 32 个 二元运算符

1
+, -, *, /, %, ^, &, |, =, <, >, <<, >>, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=, ==, !=, <=, >=, &&, ||, ,, .*, ->*

注:在折叠表达式中,所有op操作符必须相同

pack:含有未展开的参数包,且在顶层不含优先级低于转型表达式的运算符的表达式

init:不含未展开的参数包,且在顶层不含优先级低于转型表达式的运算符的表达式

():括号也是折叠表达式的一部分

...:折叠标记

一元折叠

一元右折叠

1
2
3
4
template<typename... Args>
void display(Args... args) {
((cout << args<<" "),...);
}

pack(cout << args<<" ")

op,

...:永远保持不变

一元左折叠

1
2
3
4
template<typename... Args>
void display(Args... args) {
(...,(cout << args<<" "));
}

左右区别的影响在哪里?

上面的两个示例是为了方便学习,但这容易给人一种错误的认知,就是左右折叠并无二别。

接下来我引入 - 运算符,再来试试看:

1
2
3
4
5
6
7
8
9
10
template<int...I>
constexpr int v_right = (I - ...); // 一元右折叠

template<int...I>
constexpr int v_left = (... - I); // 一元左折叠

int main(){
std::cout << v_right<4, 5, 6> << '\n'; //(4-(5-6)) 5
std::cout << v_left<4, 5, 6> << '\n'; //((4-5)-6) -7
}

二元折叠

1
2
3
4
5
6
7
8
9
// 二元右折叠
template<int...I>
constexpr int v = (I + ... + 10); // 1 + (2 + (3 + (4 + 10)))
// 二元左折叠
template<int...I>
constexpr int v2 = (10 + ... + I); // (((10 + 1) + 2) + 3) + 4

std::cout << v<1, 2, 3, 4> << '\n'; // 20
std::cout << v2<1, 2, 3, 4> << '\n'; // 20