折叠表达式是 C++17 新增的一项强大特性,它使得模板元编程中的可变参数包操作变得异常简洁。本文将从折叠表达式的语法与基本使用入手,逐步带你领略它在实际编程中的各种典型场景与高级技巧。
1. 折叠表达式到底是什么?
在 C++ 的模板编程中,我们经常需要对 Parameter Pack(可变参数包)进行迭代处理,例如对多个值求和、连接字符串、或者判断全部元素是否满足某一条件。传统方法往往需要递归实现,代码冗长、可读性差。折叠表达式用一种极其简洁的语法,让我们一次性把一个包的每个元素折叠成单一的值。
语法形式有三种:
| 折叠方式 | 例子 | 说明 |
|---|---|---|
(... op args) |
(... + nums) |
左折叠(从左到右) |
(args op ...) |
(nums * ...) |
右折叠(从右到左) |
((... op args) op ...) |
((... + nums) * ...) |
全折叠(同时使用左/右折叠) |
注意:折叠表达式只能用于 二元运算符(如 +, *, &&, || 等),且所有参数包元素必须支持该运算符。
2. 基础实例
2.1 求和
template<typename... Args>
auto sum(Args&&... args) {
return (... + args); // 左折叠
}
调用 sum(1, 2, 3, 4) 返回 10。折叠表达式省去了递归函数的书写,直观易懂。
2.2 乘积
template<typename... Args>
auto product(Args&&... args) {
return (... * args); // 左折叠
}
2.3 逻辑与
template<typename... Args>
bool all_true(Args&&... args) {
return (... && args); // 左折叠
}
若任意一个参数为 false,结果立即为 false,实现了“短路”效果。
3. 折叠表达式的高级用法
3.1 对非二元运算符的折叠
虽然折叠表达式只能用于二元运算符,但我们可以通过 包装 的方式实现对单目运算符或函数调用的折叠。例如,想对一组字符串进行拼接:
template<typename... Args>
std::string concat(Args&&... args) {
std::string result;
((result += std::forward <Args>(args)), ...); // 右折叠,逗号表达式
return result;
}
这里使用逗号表达式 (..., ...) 作为折叠表达式,将每个元素 args 通过 += 运算添加到 result。
3.2 与 initializer_list 的配合
折叠表达式可以结合 std::initializer_list 实现更灵活的算法,例如:
template<typename T>
bool any_of(const T& container, auto&& pred) {
return (false || ... || pred(container));
}
不过请注意,此处的 pred(container) 需要在编译期间是常量表达式或能够在折叠中调用。
3.3 用于 std::tuple 的遍历
template<std::size_t... Is, typename Tuple>
auto apply_sum(Tuple&& t, std::index_sequence<Is...>) {
return (... + std::get <Is>(t));
}
我们可以将 apply_sum 与 std::make_index_sequence 结合,对 std::tuple 中的所有元素求和。
4. 折叠表达式的注意事项
| 关键点 | 说明 |
|---|---|
| 1. 必须使用二元运算符 | =、+= 等单目或赋值运算符不行 |
| 2. 参与折叠的参数包必须兼容该运算符 | 否则会导致编译错误 |
| 3. 折叠表达式的优先级 | ... 在表达式中位于最右侧,遵循左到右或右到左顺序 |
| 4. 递归折叠 | 由于折叠表达式是编译期展开的,递归深度受限于编译器的递归展开深度,通常足够使用 |
5. 折叠表达式在实际项目中的应用
5.1 记录多重错误信息
在错误处理框架中,常需要把多个错误码或错误信息组合成一条完整的日志。使用折叠表达式可以轻松完成:
template<typename... Errors>
void log_errors(Errors&&... errors) {
std::ostringstream oss;
((oss << errors << ';'), ...); // 折叠每个错误并追加分号
std::cerr << oss.str() << std::endl;
}
5.2 可变参数的初始化
在自定义容器或包装器中,可能需要把可变参数传递给内部成员的构造函数:
template<typename... Args>
class Wrapper {
public:
Wrapper(Args&&... args) : data{std::forward <Args>(args)...} {}
private:
std::tuple<Args...> data;
};
这里使用参数包展开而不是折叠表达式,但折叠与参数包展开都属于模板元编程的核心工具。
6. 小结
折叠表达式为 C++17 的模板编程带来了极大的便利。它将复杂的递归展开转化为简洁的语法,让代码更易读、维护成本更低。掌握折叠表达式后,你可以在实现可变参数函数、日志系统、错误处理、以及各种泛型算法时,写出更优雅、更高效的代码。
下次再聊:如何在 C++20 中利用概念(Concepts)进一步提高可变参数函数的类型安全?