跳过正文
  1. Welcome to My Blog/

C++26 模式匹配:不用再写 std::visit 了

JekYUlll
作者
JekYUlll
C++ / Go / Linux 开发者

背景
#

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。可以嵌入 ifwhilefor 的条件里:

1
2
3
if (v match int: let i) {
    // v 当前持有 int,且 i 被绑定到值
}

选择匹配:多分支匹配并求值:

1
2
3
4
5
expression match {
    pattern1 => body1;
    pattern2 => body2;
    _ => fallback;
};

整个 match { } 块是一个表达式,可以返回值。

为什么必须写 let
#

这是 P2688 和 Rust 模式匹配最大的语法差异。

P2688 里,裸名 x 永远指代已存在的变量,let x 才是引入新绑定。原因是 C++ 的作用域规则比 Rust 复杂——有 ADL、有 using 声明、有模板依赖名,不明确标记「这里在声明新名字」会让编译器和人一起困惑。

1
2
3
4
5
int x = 42;
expr match {
    0 => /* 这里的 x 是外面的 x,值为 42 */;
    let x => /* 新的绑定 x,遮蔽了外面的 */;
};

Rust 可以省略 let,因为它的所有权模型天然区分「移动绑定」和「引用已有变量」。C++ 的赋值语义没有这种区分,所以 let 是必须的。

模式可以嵌套组合
#

这是 P2688 设计上最漂亮的一点。模式能嵌在模式里,形成递归的匹配树:

1
2
3
4
5
6
7
8
cmd match {
    Quit: _ => /* 退出 */
    Move: let [x, y] => /* 移动,解构出坐标 */
    Write: let [text] => /* 写入,解构出文本 */
    ChangeColor: [Rgb: let [r, g, b]] => /* 先匹配 ChangeColor 变体,
                                            再解构 Rgb 的三个通道 */
    ChangeColor: [Hsv: let [h, s, v]] => /* 另一变体,同样解构 */
};

编译器负责检查你是否覆盖了 ChangeColor 的所有子变体。如果漏了,编译期直接报错,不会偷偷执行到 _

六种核心模式
#

模式语法匹配条件
通配_总是匹配
常量42"hello"std::nulloptsubject == constant
绑定let xlet [a, b]总是匹配,引入新变量
可选? patternsubject 非空时继续匹配内部 pattern
备选类型Type: pattern精确类型匹配(variant 用 get<Type>,多态用 try_cast
结构化绑定[p1, p2, p3]按 tuple 协议解构

? pattern 值得单独说。它解决了一个高频场景:匹配 std::optional<T> 或裸指针。

1
2
3
4
5
6
7
std::optional<int> maybe = ...;

maybe match {
    ? let x if x > 0 => std::println("positive: {}", x);
    ? 0 => std::println("zero");
    std::nullopt => std::println("empty");
};

? 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。

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

void classify(int x) {
    x match {
        0 => std::println("zero");
        1 => std::println("one");
        2 => std::println("two");
        _ => std::println("many");
    };
}

比 switch 好在:不需要手动 break,编译器强制要求 _ 覆盖剩余情况。

接下来是 variant。C++23 的标准写法需要 overloaded 模板或 if constexpr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
std::variant<int, double, std::string> v = get_value();

std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::println("int: {}", arg);
    else if constexpr (std::is_same_v<T, double>)
        std::println("double: {:.2f}", arg);
    else
        std::println("string: {}", arg);
}, v);

P2688 下变成:

1
2
3
4
5
v match {
    int: let i       => std::println("int: {}", i);
    double: let d    => std::println("double: {:.2f}", d);
    std::string: let s => std::println("string: {}", s);
};

行数减半,没有 auto&&decay_t 的类型体操,而且编译器会验证你有没有漏掉任何一个 variant 备选项。

加上 guard 条件还能做更细的过滤:

1
2
3
4
5
6
7
v match {
    int: let i if i > 0  => std::println("positive int: {}", i);
    int: let i            => std::println("non-positive int: {}", i);
    double: let d if d != d => std::println("NaN detected");
    double: let d         => std::println("double: {}", d);
    _                     => std::println("other");
};

guards 的求值顺序是 mode → guard → body。只有模式匹配成功后 guard 才执行。

多态类型也不在话下。P2688R5 引入的 try_cast 自定义点让匹配天然支持虚基类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Shape { virtual ~Shape() = default; };
struct Circle : Shape { int radius; };
struct Rectangle : Shape { int width, height; };

int area(const Shape& s) {
    return s match {
        Circle: let [r]      => 3 * r * r;
        Rectangle: let [w, h] => w * h;
    };
}

没有 visitor 模板,没有 dynamic_cast。匹配的是编译期登记的派生类索引,不是运行时 RTTI 查找。

std::expected 也可以匹配。虽然 P2688R5 本身不直接支持 expected 的双状态,但配套论文 P3527 提供了 variant-like 协议,让 expected<T, E> 也能用 Value: let vError: 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 的人都懂。

今日可执行动作
#

  1. 装 GCC 14(sudo apt install g++-14),然后用 g++-14 -std=c++26 -fpattern-matching 编译上面 describe() 的例子,对比生成的汇编和 switch 版本是否一致
  2. 找一段你项目里的 std::visit 代码,用实验编译器改写为 match,比较行数、编译错误提示质量和编译时间
  3. 跟踪 P2688 的进展:收藏 open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2688r5.html,关注下一次 WG21 会议(Sofia, 2025 年 6 月)的投票结果

参考
#

相关文章

C++26 时代的 AI 应答:当 C++ 开始为机器学习铺路

·6 分钟
每次 AI 编程工具出新版本,评论区就有人问:“C++ 是不是快被 AI 淘汰了?” 这问题每年出现一次,跟季节一样准时。逻辑链大概是这样:AI 写 Python 又快又准 → Python 是 AI 的主力语言 → C++ 语法复杂、特性臃肿 → LLM 生成的 C++ 代码质量堪忧 → C++ 没前途。 表面看有点道理。C++ 的语法确实复杂,模板元编程的坑 AI 不一定能避开,指针错误、生命周期问题,这些对 LLM 来说全是重灾区。 但另一面是:C++ 26 标准可能是十多年来对 AI 场景最友好的一次更新。 这篇文章不讲焦虑,只讲事实:C++26 带来了哪些直接服务于 AI/ML 开发的新技术,以及委员会的提案方向在怎么回应"AI 时代"这个命题。