std::variant 让一个变量可以安全持有多种类型,但取值的类型分派始终是绕不开的样板代码。传统上要么用 std::get_if 写一串 if 判断,要么借助 std::visit 和重载 lambda。C++26 引入的 inspect 表达式把模式匹配放进语言层,让 variant 解包像 switch 一样直观,同时还能绑定内部值、检查类型组合,并保留编译期穷尽性检查。

传统 variant 访问为什么让人疲惫
std::variant 隐藏内部活跃类型,读取时必须先判断当前存的是哪一档。最简单方式是用 std::get_if 返回指针,再逐个判空。以网络连接状态为例:
#include <variant>
#include <string>
#include <iostream>
using State = std::variant<std::string, int, double>;
void handle(const State& s) {
if (const auto* p = std::get_if<std::string>(&s)) {
std::cout << "message: " << *p << '\n';
} else if (const auto* p = std::get_if<int>(&s)) {
std::cout << "code: " << *p << '\n';
} else {
std::cout << "value: " << std::get<double>(s) << '\n';
}
}
这段代码每增加一个可选项就多一条 if 分支,类型名和变量名不断重复,维护时很容易漏掉分支。更危险的是直接调用 std::get,活跃类型不匹配会抛 std::bad_variant_access。即使使用 get_if,把空值判断和业务逻辑交织在一起也会降低可读性。
另一种主流做法是 std::visit 配合 overloaded 辅助模板。标准库负责类型分派,但 lambda 参数要逐个写清,共享状态需要捕获,嵌套 variant 时 visitor 定义会进一步膨胀。编译错误也常指向标准库深处,排错成本不低。
总结起来,传统方案主要有三类问题:
- 分支数量随 variant 可选项线性增长,新增类型要手工补充;
- std::get 的异常路径容易遗漏,get_if 又到处判空;
- std::visit 需要额外访问器,嵌套类型时结构复杂。
inspect 的基础语法和匹配规则
inspect 表达式看起来像增强版 switch,写法为 inspect (expr) { pattern => result; ... }。它不会像 switch 那样丢弃表达式值,而是可以让整个 inspect 表达式返回结果。模式可以匹配具体值、类型、通配符以及解包结构。
#include <variant>
#include <string>
#include <iostream>
std::string classify(std::variant<int, double, std::string> v) {
return inspect (v) {
0 => "zero";
_ => "other";
};
}
其中 _ 是通配模式,匹配任何值但不绑定变量。若要提取 variant 中某个类型的值,可以使用类似 <int> i 的绑定模式,i 在箭头右侧直接作为 int 使用。与 std::get_if 相比,inspect 的匹配结果不会以指针形式暴露,空值判断从业务代码中消失。
更关键的是穷尽性检查。当模式没有覆盖所有可能值时,编译器会报错。这样新增 variant 成员后,所有 inspect 调用点都会在编译期提示需要补充分支,而不是发布后触发未处理异常。
用 inspect 替代 std::visit:从事件处理到状态机
事件系统是 variant 的典型使用场景。假设事件可以是鼠标点击、键盘输入或关闭窗口,传统 std::visit 需要定义一个包含三个重载的访问器。用 inspect 可以顺序写出每个分支,同时直接绑定结构体字段。
#include <variant>
#include <string>
#include <iostream>
struct MouseClick { int x; int y; };
struct KeyPress { std::string key; };
struct Close {};
using Event = std::variant<MouseClick, KeyPress, Close>;
void dispatch(const Event& ev) {
inspect (ev) {
<MouseClick> mc => std::cout << "click at " << mc.x << ", " << mc.y << '\n';
<KeyPress> kp => std::cout << "key " << kp.key << '\n';
<Close> _ => std::cout << "close\n";
};
}
与 std::visit 版本相比,这段代码不需要 overloaded 辅助模板,也不需要写 lambda 参数列表。inspect 在编译期展开分派逻辑,运行期仍然直接跳到对应分支,不会因为语法更高级而引入额外开销。如果不想使用某个值,可以用 <Close> _ 忽略;如果只需要判断类型,也可以省略变量声明。
状态机同样适合这种写法。假设连接状态由不同结构体表示,转移函数根据当前状态执行动作并返回新状态。使用 inspect 后,状态解构和转移逻辑可以放在同一处,新增状态类型时编译器会强制检查所有调用点。相比虚函数或 if constexpr,这种方案更集中,模式覆盖也更明确。
高级模式、组合与性能思考
inspect 还支持嵌套模式和守卫条件。比如 variant 内部再包含 variant,或结构体里套结构体时,可以用一条模式逐层展开。when 守卫可以在匹配后追加布尔条件,让分支细化而不破坏可读性。
#include <variant>
#include <iostream>
struct Point { int x; int y; };
struct Circle { Point center; double radius; };
struct Rect { Point top_left; Point bottom_right; };
using Shape = std::variant<Circle, Rect>;
void analyze(const Shape& s) {
inspect (s) {
<Circle> c when c.radius > 10.0 => std::cout << "large circle\n";
<Circle> c => std::cout << "small circle\n";
<Rect> r when r.top_left.x == r.bottom_right.x => std::cout << "degenerate rect\n";
<Rect> r => std::cout << "normal rect\n";
};
}
性能上,inspect 生成的分派代码通常与手写 if 链或 std::visit 处于同一量级。编译器会根据类型数量和模式复杂度把匹配转换为跳转表或比较序列。在 variant 可选项较少的场景中,它与 std::get_if 链的机器码几乎一致。对大多数项目来说,可读性和类型安全带来的收益远大于微小的运行时代价。
迁移现有项目前要确认编译器对 inspect 的支持程度,尤其是嵌套模式、守卫和错误信息。可以先在工具函数、测试代码等相对独立的模块试用,等构建链路的实现稳定后再替换复杂的 std::visit。这样既能享受模式匹配的简化效果,也能控制新特性带来的兼容风险。
C++模式匹配inspectstd::variant修改时间:2026-09-27 20:22:49