跳转到内容

什么是折叠表达式(fold expression)?

一种语法糖,提供了一种方便的语法,在变参模板的参数包上实现简单的递归运算。

先看一个不使用折叠表达式的例子。

例子:计算任意多个数的和(递归解包)

/* 计算任意多个数的和(递归解包) */
#include <iostream>
// 可以使用“完美转发”改进
template <typename T, typename... Tn>
auto sum(const T& arg1, const Tn&... args) {
if constexpr (sizeof...(args) > 0) {
return arg1 + sum(args...);
}
return arg1;
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl;
std::cout << sum<double, double>(1, 2, 3.2, 4, 5) << std::endl;
}

可以看到,这种方式使用参数包比较繁琐,并且使用时需小心谨慎,如sum<double, double>(1, 2, 3.2, 4, 5)所示。

引入折叠表达式

组成:一対小括号,一个涉及参数包的表达式,一个运算符,一个省略号。如(args + ...),其中args是参数包的名称。

理解折叠表达式:想象一下,在参数包中每个对象之间放一个运算符,得到一个表达式,返回值是该表达式的计算结果。

例子:计算任意多个数的和

/* 计算任意多个数的和 */
#include <iostream>
template <typename... Tn>
auto sum(Tn... args) {
return (args + ...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl;
std::cout << sum(1, 2, 3.2, 4, 5) << std::endl;
}

在折叠表达式(args + ...)中,操作数(operand)只有一个,就是args,一个参数包。而...不是操作数,它只是语法的一部分。

折叠表达式中的操作数,可以是任意的包含参数包表达式。这个表达式中可以只有一个参数包,也可以更复杂。

例子:计算任意多个数的平方和

/* 计算任意多个数的平方和 */
#include <iostream>
template <typename... Tn>
auto sumOfSquares(Tn... args) {
return ((args * args) + ...);
}
int main() {
std::cout << sumOfSquares(1, 2) << std::endl; // 5
std::cout << sumOfSquares(1, 2, 3) << std::endl; // 14
}

折叠表达式((args * args) + ...)中的(args * args)是这个折叠表达式的操作数,它是一个表达式。想象一下,先计算参数包中每个值的平方,然后在得到的各个结果之间放一个加号运算符,最后返回的是加法的计算结果。

例子:把参数包中的各个值发送到函数中,再対函数返回值应用折叠表达式

/*
* 把参数包中的各个值发送到函数中,再対函数返回值应用折叠表达式
* 函数什么都不做,只是打印接收到的值,不作修改直接返回
*/
#include <iostream>
int log(const int arg) {
std::cout << "Arg: " << arg << '\n';
return arg;
}
template<typename... Tn>
auto fold(Tn... args) {
return (log(args) + ...);
}
int main() {
std::cout << fold(1, 2) << std::endl;
}

这些例子只是想表达,折叠表达式中的操作数可以是任意表达式,只要包含参数包就行。

上面的例子和说明中,折叠表达式中的运算符都用的加号+,实际上也可以用其他的二元运算符。

例子,打印所有接收到的元素。逗号运算符。

/* 打印所有接收到的元素 */
#include <iostream>
template <typename T>
void log(T object) {
std::cout << object << ' ';
}
template <typename... Tn>
void print(Tn... args) {
(log(args), ...);
}
int main() {
print("hello", "world", "aoyu");
std::cout << std::endl;
print("nice", "to", "meet", "you", 3.1415926);
}

这里使用逗号运算符,。我们并不关心折叠表达式返回什么,关心的只是副作用(side effects)。

使用lambda函数代替log(),缩短代码:

template<typename... Tn>
void print(Tn... args) {
([](auto &arg) { std::cout << arg << ' '; }(args)
, ...);
}

进一步精简,使用[&args][&]让 lambda 函数直接从外部函数的作用域捕获args。这样,在 lambda 函数中不再需要参数列表(parameter list,原来的(auto &arg)),调用时也不需要再传参数(argument,原来的(args)变为了())。(语法高亮有错误提示,但实际能成功编译)

template<typename... Tn>
void print(Tn... args) {
([&]{ std::cout << args << ' '; }()
, ...);
}

一元折叠

折叠表达式有多种形式,上面例子所用的折叠表达式称为“一元折叠”(unary fold),即折叠表达式中只有一个操作数。除此之外有“二元折叠”(binary fold),即这样的折叠表达式中有两个操作数。

一元折叠表达式又有左折叠(left folds)、右折叠(right folds)之分。上面例子中所用的都是右折叠。

左折叠(... + args)和右折叠(args + ...)的区别在于,参数包中的参数的组合顺序。对于加法+,组合顺序(或者称为 折叠方向)无关紧要,但对于减法-,就必须要考虑了。

例子:对比(一元)左折叠和右折叠

/* 对比左折叠和右折叠 */
#include <iostream>
template <typename... Tn>
auto leftFold(Tn... args) {
return (... - args);
}
template <typename... Tn>
auto rightFold(Tn... args) {
return (args - ...);
}
int main() {
std::cout << leftFold(5, 10, 20); // 5 - 10 - 20, 先计算(5 - 10) = -5 = A,然后计算(A - 20) = -25
std::cout << std::endl;
std::cout << rightFold(5, 10, 20); // 5 - 10 - 20,先计算(10 - 20) = -10 = A,然后计算(5 - A) = 15
std::cout << std::endl;
}

二元折叠

在一元折叠表达式中,再添加一个操作数,就得到了二元折叠表达式(binary folds),额外添加的操作数,有时被称为初始值(initial value)。如(0 + ... + args),有两个操作数args0

与一元折叠一样,二元折叠也有左折叠((0 + ... + args))、右折叠((args + ... + 0))两种变体。

二元折叠中,运算符出现两次,但并不意味着在二元折叠中能进行两种运算,运算符出现两次只是语法上这样写罢了,位于不同位置的这两个运算符必须相同。

例子:理解二元折叠,(二元)左折叠,(二元)右折叠

#include <iostream>
template <typename... Tn>
int binaryLeftFold(Tn... args){
return (0 - ... - args);
}
template <typename... Tn>
int binaryRightFold(Tn... args){
return (args - ... - 0);
}
int main(){
std::cout << binaryLeftFold(5, 10, 20); // 0 - 5 - 10 - 20,先计算 (0 - 5) = -5 = A,再计算 (A - 10) = -15 = B,再计算 (B - 20) = -35
std::cout << std::endl;
std::cout << binaryRightFold(5, 10, 20); // 5 - 10 - 20 - 0,先计算 (20 - 0) = 20 = A,再计算 (10 - 20) = -10 = B,再计算 (5 - B) = 15
}

二元折叠的两个用途

二元折叠的两个用途:处理空参数包时不会出现编译错误;相比一元折叠更为灵活的折叠。

例子:处理空参数包,一元折叠和二元折叠对比

template <typename... Tn>
int binaryFold(Tn... args){
return (0 + ... + args);
}
template <typename... Tn>
int unaryFold(Tn... args){
return (... + args);
}
int main(){
binaryFold(); // Returns 0
unaryFold(); // Compilation error
}

使用二元折叠的函数在不带参数调用时返回0,使用一元折叠的函数在不带参数调用时出现编译错误。

例子:二元折叠,折叠<<运算符

#include <iostream>
template <typename... Tn>
void print(Tn... args){
(std::cout << ... << args);
}
int main(){
print("hello ", "world");
std::cout << std::endl;
print("nice ", "to ", "meet ", "you");
}

print("hello ", "world");实际上等价于:(std::cout << "hello ") << "world";

std::cout << "hello "返回对std::cout对象的引用,从而可以“链式”打印更多的值。

参考