背景#
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++ 的作用域规则比 Rust 复杂——有 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 完全一致的匹配语法。
代码实战#
先看最简单的:替换 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++ 自 C++17 结构化绑定以来,对日常代码风格影响最大的语法特性。毕竟写 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/

