折叠表达式(Fold Expression),低速下脚现代C++ Chapter 2 Exercise
折叠表达式(Fold Expression):低速下脚现代C++ Chapter 2 Exercise
最近在看高速上手现代C++,这是一本很不错的书,作者用仅80余页的长度介绍了C++自C++11以来至C++20带来的许多特性。这些特性给C++带来了语言可用性上的强化,运行期的强化 etc. 以及使得这门语言本身更好地支持例如函数式编程的范式。
每一章的结尾作者都会提供一些简单的小题目,其中第二章的小题目cover了折叠表达式(Fold Expression, since C++17)和结构化绑定(Structured Binding, since C++17)。在自己闲着摸索的时候发现了一个有意思的小事情,于是稍微写点东西记录一下。
感觉光看这本书和课后的题的话,内容还是不太能掌握,可能跟着其他博客看一看会比较好。
为什么需要Fold Expression?
模版是C++比较重要的一个板块之一,但是C++11之前,模版的参数数量是固定的。自从C++11之后,新引入的表示方法允许声明时引入任意数量的参数,即如下形式:
1 | template<typename... Ts> |
有了可变长模版,我们就可以写一个允许任意长度参数的函数模版了。一个显而易见的好处是,我们可用这样的语法实现以下函数功能,并且只要写一个函数模版。
1 | // 一个函数模版 mean_val,接受任意长度的参数,返回平均值 |
不过在C++17之前,这样的函数模版在解包(unpack)的时候会比较麻烦,因为我们在写函数模版的时候并不知道究竟会传几个参数进来,而参数包又不像是std::vector之类的容器有简单的方式进行遍历,因此我们对于函数参数的实际操作就变得比较麻烦。
1 | //比如说我想实现max_val,但是我并没有直观的方式遍历args的内容 |
不过目前来说,我们可以通过sizeof… 来计算参数的个数:
1 | template<typename... Ts> |
一个比较常见的做法是利用递归来进行参数解包:
1 | //书中的案例 |
这么做的确比较繁琐,而C++17中引入了一种比较简洁的方式来实现这种功能需求,这就是标题里的折叠表达式。
- 考虑下面的代码,下面的函数模版可以实现任意长度立即数的求和。
1 |
|
不过书中没有仔细讲这个折叠表达式的内容,因此原理上就有些不清晰,这里做一些补充。
折叠表达式的语法:
pack op ...
(一元右折叠 / unary right fold)... op pack
(一元左折叠 / unary right fold)pack op ... op init
(二元右折叠 / binary right fold)init op ... op pack
(二元左折叠 / binary left fold)
其中,pack
是参数包,即上述代码中的t,op即operator,支持32种二元运算符,例如+,-,*,/
, init
表示最后一层展开时二元运算符的另一个操作数。 其中在二元折叠中两个operator要保持一致。
以上四个表达式和以下内容等价:
1 | (args op ...) <-> (arg1 op (arg2 op (arg3 op (... op (arg_n-1 op arg_n))))) |
这么写可能还是有点晕,举两个例子可能更好:
1 | args = [1,2,3] |
下面是一个注意点,由于运算符优先级问题,第一种备注是掉的写法并不被接受,需要写成第二种写法
1 | template<typename ...Args> |
作业题:
利用折叠表达式写一个计算求均值的函数模版:
答案如下:
1 | template <typename... T> |
但是…
不知道为什么我突然想试着把 +
改成 -
1 | (t - ...)/sizeof...(t) <-> (1-(2-(3-(4-5)))) = (int)3/5 = 0 |
这个例子倒是没有问题,如果我改成左折叠:
1 | return (... - t) / sizeof...(t); |
这个一长串数字并不是随机数,运行了好几次都是同一个结果。但是:
1 | (... - t)/sizeof...(t) <-> ((((1-2)-3)-4)-5)/5 = -13/5 |
本应该输出-2才对,那么究竟是哪里出问题了呢?
探索
首先我查看了折叠表达式和sizeof…(t)
的返回值,用lldb调试后发现数值没有问题,那么应该是除法这一步出了问题。
当我将p的类型从auto改为int之后,返回值就变成-2了:
那么看来应该是sizeof…
返回值的问题。
解释:
询问学长之后发现,sizeof...
返回的是一个size_t
类型的值,在64-bit操作系统中类型是uint64_t
,即64位无符号整型。然后折叠表达式自动推断返回为int(int32_t
), 二者做除法的时候int被提升到uint64_t
,然后又因为负数的符号位在最高位,被转换成unsigned的时候,数值就变得非常大了。
可以具体算一下这个3689348814741910320
的由来,结果是一致的。
1 | -13 = (hex) FFFFFFFFFFFFFFF3 -> 18,446,744,073,709,551,603 |
所以需要注意的是,一行的表达式,其实包含了一次类型推断和一次隐式类型转换,所以使用的时候需要比较小心一些。
1 | (t + ...) / sizeof...(t); |
等我看完Chapter 4的时候可能会再来写一点内容,感觉还是得一边看一边跟着例子写一点才好。