C++折叠表达式

C++17 为模板元编程带来了一个非常有用的特性:折叠表达式(fold expressions)。它的出现让变参模板函数的编写变得更加简洁、清晰和直观。

1. 如何处理变长参数包

在 C++11/14 中,如果我们要处理变长参数包(parameter pack),通常需要递归展开,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
void print(const T& t) {
    std::cout << t << '\n';
}

template<typename T, typename... Args>
void print(const T& t, const Args&... args) {
    std::cout << t << ", ";
    print(args...);
}

虽然这段代码能完成任务,但却略显冗长,并且生成的汇编代码也会很多。C++17 中引入的折叠表达式,就是为了解决这个问题。

可以在 godbolt 上看看两种方法生成的汇编代码:https://godbolt.org/z/55a7q85oE

2. 什么是折叠表达式

折叠表达式是一种用运算符对参数包进行折叠的方式。折叠表达式的实例化按以下方式展开成表达式 e:

(图片来源:https://zh.cppreference.com/w/cpp/language/fold

基本语法形式:

类型语法形式示例
一元左折叠(... op pack)(... + args)
一元右折叠(pack op ...)(args + ...)
二元左折叠(init op ... op pack)(0 + ... + args)
二元右折叠(pack op ... op init)(args + ... + 0)

折叠表达式支持多种运算符,包括但不限于:

  • 算术运算符:+, -, *, /
  • 逻辑运算符:&&, ||
  • 比较运算符:==, !=, <, >
  • 位运算符:&, |, ^
  • 逗号运算符:,

需要注意的是:

  1. 若参数包为空,使用二元折叠可设置初始值防止编译错误
  2. 初始值放在哪边,就由哪一边“开始结合”

3. 折叠表达式示例

3.1 求和函数

1
2
3
4
template<typename... Args>
auto sum(Args... args) {
    return (... + args); // 左折叠
}

使用:

1
std::cout << sum(1, 2, 3, 4); // 输出 10

3.2 打印变参

1
2
3
4
template<typename... Args>
void print(Args&&... args) {
    (std::cout << ... << args) << '\n'; // 左折叠结合流输出
}

使用:

1
print("Hello, ", "fold ", "expression!"); // 输出 Hello, fold expression!

3.3 所有参数是否都满足某条件

1
2
3
4
template<typename... Args>
bool all_true(Args... args) {
    return (... && args);
}

使用:

1
bool result = all_true(true, true, false); // false

4. 与 std::cout 结合

std::cout << a << b << c 是一个左结合的表达式,其等价于:

1
((std::cout << a) << b) << c

因此,在使用折叠表达式来打印参数时,必须使用左折叠!!!

正确示例为:

1
2
3
4
template<typename... Args>
void print(Args&&... args) {
    (std::cout << ... << args); // 左折叠
}

这个展开方式等价于:

1
(((std::cout << arg1) << arg2) << arg3) ...

这样每次都把输出结果“继续”传给 std::cout << ...保持流操作链的有效性

但如果按下面的写法来:

1
(std::cout << args << ...); 

会导致编译错误或者语义错误,因为 std::cout 必须放在最左边,才能正确启动流操作链。

5. 与 std::cout 和逗号运算符结合

对于逗号运算符:在 (a, b) 中,逗号运算符会先执行 a,再执行 b,返回 b 的值。这允许我们:

  • 在一次折叠中 执行多条语句
  • 且确保顺序执行、无中间变量展开

例如下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

template<typename... Args>
void print(Args... args) {
    ((std::cout << args << ", "), ...);
}

int main() {
    print(1, 3.0, '*');
}

其在编译器处理之后会是这个样子(使用这个工具 https://cppinsights.io/):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

template<typename ... Args>
void print(Args... args) {
  (((std::cout << args) << ", "), ...);
}

template<>
void print<int, double, char>(int __args0, double __args1, char __args2) {
  (std::operator<<(std::cout.operator<<(__args0), ", ")) , 
  (
    (std::operator<<(std::cout.operator<<(__args1), ", ")) ,   
    (std::operator<<(std::operator<<(std::cout, __args2), ", "))
  );
}

int main() {
  print(1, 3.0, '*');
  return 0;
}

值得注意的是:看起来 ...args 的右边,但这依然是左折叠

使用 Hugo 构建
主题 StackJimmy 设计