C++14 写一个变参 print 函数需要几步?答案是:至少 50 行递归模板代码。C++17 的折叠表达式让这件事变成一行。
一、变参模板的痛:递归展开的噩梦
在 C++14 之前(或不用 fold expressions 时),处理变参模板需要递归展开:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream>
void print() { std::cout << '\n'; }
template<typename T, typename... Args> void print(T first, Args... rest) { std::cout << first; if (sizeof...(rest) > 0) { std::cout << ", "; print(rest...); } else { std::cout << '\n'; } }
int main() { print(1, "hello", 3.14, "world"); }
|
问题:
- 需要两个函数(递归终止 + 递归展开)
- 每个参数都要实例化一套模板
- 代码难懂,维护成本高
- 编译错误信息经常让人摸不着头脑
二、折叠表达式:四种形式
C++17 引入折叠表达式,有 4 种形式:
| 形式 | 表达式 | 展开结果 |
|---|
| Unary Right | (args op ...) | arg0 op (arg1 op (... op argN)) |
| Unary Left | (... op args) | ((arg0 op arg1) op ...) op argN |
| Binary Right | (args op ... op init) | arg0 op (arg1 op (... op (argN op init))) |
| Binary Left | (init op ... op args) | (((init op arg0) op arg1) op ...) op argN |
graph LR
A["(args + ...)"] --> B["Unary Right"]
A --> C["arg0 + (arg1 + (arg2 + ...))"]
D["(... + args)"] --> E["Unary Left"]
D --> F["((arg0 + arg1) + arg2) + ..."]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style D fill:#FFDAB9,stroke:#FFAB76,color:#333
三、一行搞定变参打印
1 2 3 4 5 6 7 8 9 10 11 12
| #include <iostream>
template<typename... Args> void print(Args&&... args) { (std::cout << ... << std::forward<Args>(args)) << '\n'; }
int main() { print(1, "hello", 3.14, "world"); }
|
(std::cout << ... << std::forward<Args>(args)) 展开为:
1
| ((std::cout << arg1) << arg2) << ... << argN
|
完美转发 + 折叠表达式 = 优雅的变参处理。
四、实际应用场景
场景 1:求和函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| template<typename... Args> auto sum(Args... args) { return (args + ...); }
template<typename... Args> auto sum_left(Args... args) { return (... + args); }
template<typename... Args> auto sum_with_default(Args... args) { return (args + ... + 0); }
int main() { std::cout << sum(1, 2, 3, 4, 5) << '\n'; std::cout << sum_left(1, 2, 3, 4, 5) << '\n'; std::cout << sum_with_default(1, 2, 3) << '\n'; }
|
场景 2:逻辑运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| template<typename... Conds> constexpr bool all_of(Conds... conds) { return (... && conds); }
template<typename... Conds> constexpr bool any_of(Conds... conds) { return (... || conds); }
template<typename T, typename... Preds> bool matches_all(const T& val, Preds... preds) { return (preds(val) && ...); }
int main() { std::cout << all_of(true, true, true) << '\n'; std::cout << all_of(true, false, true) << '\n'; std::cout << any_of(false, false, true) << '\n'; std::cout << any_of(false, false, false) << '\n'; bool r = matches_all(5, [](int x){ return x > 0; }, [](int x){ return x < 10; }, [](int x){ return x % 2 == 1; } ); std::cout << "matches: " << r << '\n'; }
|
场景 3:完美转发参数包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <memory>
template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); }
struct Widget { int x, y, z; template<typename... Ints> Widget(Ints... vals) : x(vals)... { static_assert(sizeof...(Ints) <= 3, "Too many arguments"); } };
|
五、C++14 vs C++17:方案对比
变参求和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
template<typename T> T sum() { return T{}; }
template<typename T, typename... Args> T sum(T first, Args... rest) { return first + sum<T>(rest...); }
template<typename... Args> auto sum(Args... args) { return (args + ...); }
|
flowchart TD
subgraph "C++14 递归方案"
A1["sum(1,2,3,4,5)"] --> A2["1 + sum(2,3,4,5)"]
A2 --> A3["2 + sum(3,4,5)"]
A3 --> A4["3 + sum(4,5)"]
A4 --> A5["4 + sum(5)"]
A5 --> A6["5 + sum()"]
A6 --> A7["5 + 0 = 5"]
A7 --> A8["9"]
A8 --> A9["13"]
A9 --> A10["15"]
end
subgraph "C++17 Fold 方案"
B1["(1 + 2 + 3 + 4 + 5)"] --> B2["编译器直接展开"]
B2 --> B3["15"]
end
style A1 fill:#FFDAB9,stroke:#FFAB76
style B1 fill:#B5EAD7,stroke:#80CBC4
style B3 fill:#B5EAD7,stroke:#80CBC4
完整代码对比
| 维度 | C++14 递归 | C++17 Fold |
|---|
| 代码量 | 10+ 行 | 1-2 行 |
| 模板实例化次数 | O(N) | O(1) |
| 编译时间 | 较慢 | 更快 |
| 可读性 | 差 | 好 |
| 维护成本 | 高 | 低 |
六、常见陷阱
陷阱 1:空参数包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
template<typename... Args> bool all_true(Args... args) { return (... && args); }
static_assert(all_true() == true);
template<typename... Args> auto sum(Args... args) { return (args + ...); }
|
陷阱 2:优先级问题
1 2 3 4 5 6 7 8 9
| template<typename... Args> void print(Args... args) { (std::cout << ... << args); }
|
陷阱 3:二元折叠的初始值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template<typename... Args> auto sum(Args... args) { return (args + ... + 0); }
template<typename... Args> auto concat(Args... args) { return (args + ... + std::string{}); }
|
七、综合实例:类型安全的 printf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <iostream> #include <string>
template<char... Cs> struct FormatString { static constexpr char value[] = {Cs...}; };
void print_impl() {}
template<typename T, typename... Args> void print_impl(T first, Args... rest) { std::cout << first; if constexpr (sizeof...(rest) > 0) { print_impl(rest...); } }
template<typename... Args> void println(Args... args) { print_impl(args...); std::cout << '\n'; }
template<typename T, typename... Preds> constexpr bool satisfies_all(T val, Preds... preds) { return (... && preds(val)); }
int main() { println("Hello,", "World!", 42, 3.14); constexpr bool valid = satisfies_all(5, [](int x){ return x > 0; }, [](int x){ return x < 100; } ); static_assert(valid, "5 应该满足所有条件"); std::cout << "All checks passed!\n"; }
|
折叠表达式是 C++17 最实用的新特性之一。一行 (pack op ...) 替代几十行递归模板代码,不仅更简洁,编译更快,生成的错误信息也更加友好。变参模板从此不再可怕。
C++17 新特性系列完结:
- ✅ Structured Bindings:告别临时变量
- ✅ if constexpr:编译期分支利器
- ✅ Inline Variables:ODR 问题终结者
- ✅ Fold Expressions:变参模板救星
有问题或建议?欢迎留言讨论!
📚 C++17 新特性 系列导航
本文是《C++17 新特性》系列第 4/8 篇。
📖 全部 8 篇目录(点击展开)
- (一)Structured Bindings
- (二)if constexpr
- (三)Inline Variables 与 constexpr 加强
- (四)Fold Expressions ← 当前
- (五)std::optional / variant / any
- (六)std::apply / std::invoke
- (七)Filesystem 大全
- (八)Attribute 新增
本章讲 C++17 的 Fold Expressions(折叠表达式),对比维度:本特性 vs C++11/14 旧写法 vs 其他语言变参处理。
对比分析
一、本特性 vs 旧写法
| 维度 | C++17 fold 写法 | C++14 递归展开 | 影响 |
|---|
| 变参打印 | (std::cout << ... << args) | 写一个 base case + 递归 | 一行替代 30+ 行 |
| 变参求和 | (args + ... + 0) | 递归 + 特化 | 一行搞定 |
| 变参布尔与 | (args && ...) | 同样递归 | 一行搞定 |
| 编译期处理 | 可与 if constexpr 组合 | 模板技巧复杂 | 表达力强 |
二、对比其他语言
| 语言 | 变参处理 | 是否支持 fold | 备注 |
|---|
| C++17 | ✅ 一元 / 二元 fold | ✅ | 编译期可推导 |
| Java | 可变参数 T... | ❌ | 无 fold 语义 |
| Python | *args | ✅ 通过 functools.reduce | 运行时 |
| Rust | 宏 + 迭代器 | ✅ 通过宏变参 | 比 fold 更”宏” |
| Haskell | 类型类 foldr/foldl | ✅ 语言级 | 类型系统支撑最强 |
三、优缺点
优点:
- 大幅缩短变参模板代码
- 与
if constexpr 配合可写出简洁 SFINAE-free 代码
缺点:
- 一元 fold 在空包时需要显式 identity(
+ 0、&& true 等) - 调试栈不如递归直观
四、何时选
- 任何”对所有参数做同一操作”的场景:首选 fold
- 需要复杂逻辑(如先转换再聚合):回到递归或 fold + lambda