C++17折叠表达式
800 Words | Read in about 4 Min | View times
Overview
C++11引入了可变参数模板(variadic template),它可以接收任意数量的模板参数,但是参数包不能直接展开,需要通过递归或者逗号表达式的方式进行展开,写法非常繁琐。C++17对这个问题进行了优化,引入了折叠表达式的概念,用来简化对可变参数模板中参数包的展开过程。本节内容重点介绍折叠表达式的使用方法。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
引子
考虑一个场景,我们需要对逐一打印不定长的参数,C++17之前常见的做法为定义一个可变参数模板函数进行处理。
可变参数中的参数包(parameter pack)需要展开,在C++17之前是不能直接展开的,需要借助递归或者逗号表达式来实现。
递归展开参数包
1//C++11,应对单参数的递归出口
2void print() {
3 std::cout << std::endl;
4}
5
6template<typename T, typename... Ts>
7void print(T arg, Ts... args) {
8 std::cout << arg << ", ";
9 print(args...);
10}
11
12int main() {
13 print("hello", "zhxilin", 1, 2, 3);
14 return 0;
15}
我们定义了一个可变参数模板函数,C++17之前没办法直接进行参数包展开。
那么为了参数包展开,需要采用递归思路来展开。第一个print
为递归出口,第二个print
为递归调用逻辑。
逗号表达式展开参数包
参数包展开还可以使用逗号表达式来展开:
1template<typename T>
2void print_item(T arg) {
3 std::cout << arg << ", ";
4}
5
6template<typename... T>
7void expand(T... args) {
8 int arr[] = { (print_item(args), 0)... }; //逗号表达式
9 std::cout << std::endl;
10}
11
12int main() {
13 expand("hello", "zhxilin", 1, 2, 3);
14 return 0;
15}
回顾一下逗号表达式a = (b = c, d)
,表达式的执行顺序是,先进行b = c
的赋值,接着括号中的逗号表达式返回d
,因此最终表达式a
等于d
。
所以上面的例子中,int arr[] = { (print_item(args), 0)... };
语句,其实核心部分是这一句(print_item(args), 0)
,先指定print_item()
函数,然后逗号表达式返回0
;然后利用初始化列表(initialize list)来初始化一个初值都是0
、大小为sizeof...(args)
的数组。通过初始化列表,最终展开为
1int arr[] = {
2 (print_item(args0), 0),
3 (print_item(args1), 0),
4 (print_item(args2), 0),
5 ...
6};
可以看出,在C++17之前,可变参数模板的参数包展开非常麻烦,所以C++17引入了折叠表达式,来简化参数包的展开过程。
折叠表达式
折叠表达式用来简化参数包的展开过程。
语法规则
折叠表达式有4种语法形式:
1(pack op ...) //一元右折叠
2
3(... op pack) //一元左折叠
4
5(pack op ... op init) //二元右折叠
6
7(init op ... op pack) //二元左折叠
其中各个部分的含义如下:
op
折叠表达式支持以下32个二元运算符:
+
, -
, *
, /
, %
, ^
, &
, |
, =
, <
, >
, <<
, >>
, +=
, -=
, *=
, /=
, %=
, ^=
, &=
, |=
, <<=
, >>=
, ==
, !=
, <=
, >=
, &&
, ||
, ,
, .*
, ->*
在折叠表达式中,所有op
操作符必须相同。
pack
含有未展开的参数包,且在顶层不含优先级低于转型表达式的运算符的表达式。
init
不含未展开的参数包,且在顶层不含优先级低于转型表达式的运算符的表达式。
...
折叠标记。
( )
要注意,括号也是折叠表达式的一部分。
一元折叠
一元折叠分为一元右折叠和一元左折叠。
假设表达式是E
(包含参数包),操作符是op
:
- 一元右折叠
形如(E op ...)
的折叠表达式称为一元右折叠。
一元右折叠展开之后的含义是(E1 op (... op (En-1 op En)))
。
- 一元左折叠
形如(... op E)
的折叠表达式称为一元左折叠。
一元左折叠展开之后的含义是(((E1 op E2) op ...) op En)
。
最简单的折叠表达式是一个求和函数:
1//一元右折叠
2template<typename... T>
3auto sumR(T... args) {
4 return (args + ...);
5}
6
7//一元左折叠
8template<typename... T>
9auto sumL(T... args) {
10 return (... + args);
11}
12
13int main() {
14 std::cout << sumR(1, 2, 3, 4, 5) << std::endl; //15
15 std::cout << sumL(1, 2, 3, 4, 5) << std::endl; //15
16 return 0;
17}
sumR(1, 2, 3, 4, 5)
右折叠展开后等价于1 + (2 + (3 + (4 + 5)))
;sumL(1, 2, 3, 4, 5)
左折叠展开后等价于(((1 + 2) + 3) + 4) + 5
。
在这个例子中,左右折叠对于加法和乘法的结果都是相等的;但是有些情况下,比如减法和除法,左右折叠展开的结果是不同的,要谨慎使用。
我们再回头看一下引子中的打印问题,使用折叠表达式改写之后应该是这样的:
1//一元右折叠
2template<typename... T>
3void printR(T... args) {
4 ((std::cout << args << ", "), ...) << std::endl;
5}
6
7//一元左折叠
8template<typename... T>
9void printL(T... args) {
10 (..., (std::cout << args << ", ")) << std::endl;
11}
12
13int main() {
14 printR("hello", "zhxilin", 1, 2, 3);
15 printL("hello", "zhxilin", 1, 2, 3);
16 return 0;
17}
在上面这个例子的折叠表达式中,op
是逗号,
,E
是输出语句std::cout << args << ", "
,所以其实是一个逗号表达式的折叠表达式。注意E
语句中<<
操作符的返回结果是std::ostream
,所以可以继续输出流操作。
通过折叠表达式,原来通过递归或复杂逗号表达式的展开方式就可以消除了。
二元折叠
二元折叠分为二元右折叠和二元左折叠。
假设表达式是E
(包含参数包),操作符是op
,初值是I
:
- 二元右折叠
形如(E op ... op I)
的折叠表达式称为二元右折叠。
二元右折叠展开之后的含义是(E1 op (... op (En-1 op (En op I))))
。
- 二元左折叠
形如(I op ... op E)
的折叠表达式称为二元左折叠。
二元左折叠展开之后的含义是((((I op E1) op E2) op ...) op En)
。
再看一下前面通过一元折叠表达式实现的累加函数,这种实现已经可以满足大多数情况了,但是有一种情况没法满足,就是函数传递了空参数。如果非要用一元折叠表达式实现,且支持传递空参数,那么需要对模板进行特化:
1//一元右折叠
2template<typename... T>
3auto sumR(T... args) {
4 return (args + ...);
5}
6auto sumR() { return 0; }
7
8//一元左折叠
9template<typename... T>
10auto sumL(T... args) {
11 return (... + args);
12}
13auto sumL() { return 0; }
14
15int main() {
16 std::cout << sumR() << std::endl; //0
17 std::cout << sumL() << std::endl; //0
18 return 0;
19}
虽然可行,但是还需要实现特化版本,还是比较麻烦的。更简单的方式是使用二元折叠表达式给定初值:
1//二元右折叠表达式
2template<typename... T>
3auto sumR(T... args) {
4 return (args + ... + 0);
5}
6
7//二元左折叠表达式
8template<typename... T>
9auto sumL(T... args) {
10 return (0 + ... + args);
11}
12
13int main() {
14 std::cout << sumR() << std::endl; //0
15 std::cout << sumL() << std::endl; //0
16 std::cout << sumR(1, 2, 3, 4, 5) << std::endl; //15
17 std::cout << sumL(1, 2, 3, 4, 5) << std::endl; //15
18 return 0;
19}
如此一来,只需要定义一个模板函数即可,不需要定义特化版本,就可以实现传空参数的累加求和了。
注意事项
从累加求和的一元折叠表达实现中我们得到结论,累加时一元折叠表达式不支持空参数的参数包展开,必须实现一个特化版本。
但是对于一元折叠表达式,以下三种操作符可以直接支持空参数的参数包展开:
-
&&
,逻辑与。空包的值代表true
。 -
||
,逻辑或。空包的值代表false
。 -
,
,逗号运算符。空包的值代表void()
。
1template<typename... T>
2bool all(T... args) { return (... && args); }
3
4template<typename... T>
5bool any(T... args) { return (... || args); }
6
7template<typename TFunc, typename... Ts>
8void for_each(TFunc&& f, Ts&&... args) {
9 (f(args), ...);
10}
11
12int main() {
13 std::cout<< std::boolalpha;
14
15 bool b1 = all();
16 std::cout << "all() = " << b1 << std::endl; //true
17
18 bool b2 = all(true, false, true);
19 std::cout << "all(true, false, true) = " << b2 << std::endl; //false
20
21 bool b3 = any();
22 std::cout << "any() = " << b3 << std::endl; //false
23
24 bool b4 = any(true, false, true);
25 std::cout << "any(true, false, true) = " << b4 << std::endl; //true
26
27 for_each([](auto a){ std::cout << a << std::endl; }, 1, 2, 3); //逐行输出
28 for_each([](auto a){ std::cout << a << std::endl; }); //没有任何输出
29
30 return 0;
31}
从运行结果可以看到:
一元折叠表达式中,空包展开时,&&
的结果默认为true
;
一元折叠表达式中,空包展开时,||
的结果默认为false
;
一元折叠表达式中,空包展开时,,
的结果默认为void()
,因此for_each()
其实没有执行任何操作。