背景#
C++ 里做类型分派,一直很啰嗦。
std::variant 从 C++17 就有了,但要用它就得写 visitor。要么搞一个 overloaded 模板类,要么写一组 lambda,把每种类型都列出来。表达的逻辑不复杂,样板代码先堆了三倍。我每次写完都怀疑自己是在写业务,还是在安抚编译器。
其他语言早就把这个问题处理掉了。Rust 有 match,Haskell 解构类型构造子是日常,Python 3.10 也加了 match-case。C++ 的 switch 只能匹配整数和枚举;if-else 链遇到 variant 又丑又容易漏分支,编译器也帮不上忙。
P2688 想补上这块。从 2021 年 Michael Park 提交第一版,到现在已经修了五个大版本。目标是给 C++26 引入原生 match 表达式:类型分派、值匹配、结构化绑定,一次写清楚。不需要 visitor 模板,不需要 if constexpr 判断类型索引。
这事重要,是因为 variant 在 C++ 代码里越来越常见。错误处理用 std::expected,状态机用 variant,AST 节点也常用 variant。每多写一次 visitor,就多一个漏分支和写错类型的机会。
原理#
P2688 的 match 表达式分两种用法。
单次匹配:expression match pattern,返回 bool。可以放进 if、while、for 条件里:
| |
选择匹配:多分支匹配并求值:
| |
整个 match { } 块是表达式,可以返回值。
为什么必须写 let#
这是 P2688 和 Rust 模式匹配最大的语法差异。
P2688 里,裸名 x 永远指代已有变量,let x 才引入新绑定。原因很现实:C++ 的作用域规则太复杂,有 ADL、using 声明、模板依赖名。如果不明确标记“这里在声明新名字”,编译器和读代码的人都会难受。
| |
Rust 可以省略 let,因为它的所有权模型天然区分“移动绑定”和“引用已有变量”。C++ 的赋值语义没有这种区分,所以 let 必须写。
模式可以嵌套组合#
P2688 设计里我最喜欢的是这一点:模式能套在模式里,形成递归匹配树。
| |
编译器会检查 ChangeColor 的所有子变体是否都被覆盖。漏了就编译期报错,不会悄悄落到 _。
六种主要模式#
| 模式 | 语法 | 匹配条件 |
|---|---|---|
| 通配 | _ | 总是匹配 |
| 常量 | 42、"hello"、std::nullopt | subject == constant |
| 绑定 | let x、let [a, b] | 总是匹配,引入新变量 |
| 可选 | ? pattern | subject 非空时继续匹配内部 pattern |
| 备选类型 | Type: pattern | 精确类型匹配(variant 用 get<Type>,多态用 try_cast) |
| 结构化绑定 | [p1, p2, p3] | 按 tuple 协议解构 |
? pattern 值得单独看。它处理的是高频场景:匹配 std::optional<T> 或裸指针。
| |
? pattern 先检查 subject 是否有值(bool(subject)),有值才继续匹配内部模式。空 optional 直接跳到下一个分支。P2688R5 的 §3.2 对这块有完整的形式化定义。
底层怎么编译#
底层实现上,模式匹配会编译成跳转表加类型索引,不需要 RTTI。
对于 std::variant<int, double, std::string>,编译器在编译期就知道三个备选类型。int: let i 分支编译成“如果 variant 的 index() == 0,取第 0 个元素赋给 i”。double: let d 对应 index() == 1。没有动态分发,没有虚函数调用,和手写 if-constexpr 链生成的机器码相同。
exhaustiveness 检查也在编译期完成:编译器枚举所有可能的 variant index,验证每个 index 都有对应分支。漏了就是 hard error。对于多态类型,P2688R5 把匹配机制从 std::cast 改成 ADL try_cast,第三方库可以给自定义 sum type 实现自己的 try_cast,获得和 std::variant 一致的匹配语法。
用 match 改写一段 std::visit#
先看最简单的:替换 switch。
| |
比 switch 省心:不用手写 break,编译器强制要求 _ 覆盖剩余情况。
再看 variant。C++23 的标准写法需要 overloaded 模板或 if constexpr:
| |
P2688 下变成:
| |
行数少了不少,也没有 auto&& 和 decay_t 的类型体操。更关键的是,编译器会验证你有没有漏掉任何 variant 备选项。
加上 guard 条件,还能做更细的过滤:
| |
guards 的求值顺序是 mode → guard → body。只有模式匹配成功后,guard 才会执行。
多态类型也能写。P2688R5 引入的 try_cast 自定义点,让匹配支持虚基类:
| |
没有 visitor 模板,没有 dynamic_cast。这里匹配的是编译期登记的派生类索引,不是运行时 RTTI 查找。
std::expected 也可以纳入这套语法。P2688R5 本身不直接支持 expected 的双状态,但配套论文 P3527 提供了 variant-like 协议,让 expected<T, E> 能用 Value: let v 和 Error: let e 分派。
标准化进展#
P2688 的标准化过程并不轻松。
2021 年 R0 首次提交。2024 年 3 月东京会议上,R1 获得 EWG 明确支持:34-9-0-0-0。到 R3/R4 时,讨论变细,分歧也开始变大。2024 年 10 月远程投票结果是 13-3-1-0-1(喜欢方向);11 月全体 EWG 投票变成 17-16-6-1-9:鼓励继续往 C++26 推进,但反对票已经接近半数。
反对意见主要有两类:一类担心语法太复杂,尤其是嵌套模式加 guard 的可读性;另一类希望更快收尾,把精力留给 Reflection 和 Contracts。R4 到 R5 的主要变化,是把变体匹配底层从 std::cast 改成 ADL try_cast,给开放 sum type(非典型 variant 的用户自定义类型)留扩展空间。
编译器支持现状:
| 编译器 | 支持 | 备注 |
|---|---|---|
| GCC 14+ | 实验性 | -std=c++26 -fpattern-matching |
| Clang 18+ | 实验性 | 同上,有部分实现差异 |
| MSVC | 暂无 | 等 wording 稳定 |
进度明显慢于同期的 Contracts(P2900)和 Reflection(P2996),但 Michael Park 仍在推进。配套论文 P3521 提供开放 sum type 的自定义匹配协议,P3527 覆盖 std::expected 和 variant-like 类型。
如果 P2688 最终进 C++26,它会显著改变日常 C++ 代码风格。毕竟写 visitor 的痛苦,每个用过 variant 的人都懂。
今天就能做的事#
- 装 GCC 14(
sudo apt install g++-14),然后用g++-14 -std=c++26 -fpattern-matching编译上面describe()的例子,对比生成的汇编和 switch 版本是否一致 - 找一段你项目里的
std::visit代码,用实验编译器改写为match,比较行数、编译错误提示质量和编译时间 - 跟踪 P2688 的进展:收藏 open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2688r5.html,关注下一次 WG21 会议(Sofia, 2025 年 6 月)的投票结果
参考#
- P2688R5: Pattern Matching:
matchExpression:https://open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2688r5.html - P2688R5 mirror on isocpp.org:https://isocpp.org/files/papers/P2688R5.html
- P3521R0: Customization Point for Open Sum Types:https://open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3521r0.html
- Markaicode: C++26 Pattern Matching Deep Dive:https://markaicode.com/pattern-matching-cpp26/
- cppstat: C++26 Feature Support:https://cppstat.org/

