std::variant 扩展:sum type、设计模式与异构容器
上一篇 variant_visit.md 以 访问者(Visitor) 为主线,讲清
variant+visit如何表达闭集联合并做类型安全分派。
不必把variant理解成「只为访问者服务」。 它本质是 闭集联合类型(sum type / tagged union):在类型里写死「当前只能是 A、B、C 之一」。很多场景里都有「多种形态、同一槽位」——variant往往比继承 + 虚函数更直接。
本文是续篇:按与variant贴得有多紧梳理 State / Strategy / Command / ADT 等用法,并点到 Factory、Composite 等弱相关模式;再衔接optional/expected、异构vector与 crtp.md 的选型对照。
1. 先分清两件事
| 问题 | 典型 API | 本文 / 前一篇 |
|---|---|---|
| 当前是哪一种?(数据建模) | std::variant<A,B,C>、emplace、holds_alternative | 本篇重心 |
| 对这一种做什么?(操作分派) | std::visit、Visitor、overload | 主要在 variant_visit.md |
variant 回答:「槽位里现在是 A、B 还是 C?」
visit 回答:「对当前这一种,执行哪段逻辑?」
扩展篇侧重第一种:用 variant 表达「多形态之一」。下面按贴得有多紧组织,不是硬套 GoF 名词表。
1.1 和「可辨识联合」的关系
std::variant<int, std::string> v;
内存语义(概念上):
┌─────────────┬──────────────────┐
│ tag / index │ 当前激活的 payload │
│ 0 或 1 │ int 或 string 值 │
└─────────────┴──────────────────┘
同一时刻只有一个 alternative 有效——这就是 sum type:类型的「和」是几种形态之一,而不是几种字段同时存在(那是 product type / struct)。
2. 状态(State)
状态机里,不同状态往往携带不同字段(而不只是枚举值 + 一个大 struct 里塞可选成员)。
#include <variant>
struct Idle {};
struct Running { int progress; };
struct Error { int code; std::string msg; };
using MachineState = std::variant<Idle, Running, Error>;
void transition(MachineState& s) {
if (std::holds_alternative<Running>(s)) {
auto& r = std::get<Running>(s);
if (r.progress >= 100) s = Idle{};
}
}
- 切换状态 = 赋一个新的 alternative(
s = Error{404, "not found"})。 - 对状态的行为可用
visit(见前一篇),也可在转移逻辑里用holds_alternative/get_if。
Idle ──start──▶ Running ──done──▶ Idle
│
└──fail──▶ Error
与经典 State(每个状态一个类 + 虚函数)相比:闭集、值语义、无 vptr;状态种类会频繁插件式增加时,仍更适合虚接口。
3. 策略(Strategy)
把可互换算法收成联合类型:
struct Gzip { int level; };
struct Lz4 {};
struct NoCompress {};
using Compressor = std::variant<Gzip, Lz4, NoCompress>;
size_t compress(const Compressor& c, std::span<const std::byte> in, std::span<std::byte> out) {
return std::visit([&](const auto& strategy) -> size_t {
using T = std::decay_t<decltype(strategy)>;
if constexpr (std::is_same_v<T, Gzip>) { /* ... */ return 0; }
else if constexpr (std::is_same_v<T, Lz4>) { /* ... */ return 0; }
else return in.size(); // NoCompress: 原样
}, c);
}
- 选策略 = 构造/赋值某种
Compressor。 - 执行 =
visit或if constexpr泛型 lambda(C++17+)。
策略种类固定且希望内联、少堆时很合适;需要运行时加载 .so 插件时,换接口 + 虚函数或类型擦除。
4. 命令(Command)
撤销栈、事件总线、游戏输入队列:
struct Insert { int pos; char c; };
struct Delete { int pos; };
struct MoveCursor { int delta; };
using Command = std::variant<Insert, Delete, MoveCursor>;
std::vector<Command> history;
void apply(const Command& cmd, Editor& ed) {
std::visit([&ed](const auto& c) { /* 对 ed 应用 c */ }, cmd);
}
每条命令一种类型,vector<Command> 即异构命令序列(见 §7)。回放、序列化时 often 给每个 alternative 配稳定 tag 或手写编解码。
5. 解释器 / AST(代数数据类型 ADT)
表达式树、配置 DSL、JSON 子集在函数式语言里叫 ADT:
struct Literal { double value; };
struct Add;
struct Mul;
// 前向声明 + variant 持有子表达式(递归)
struct Expr;
using ExprPtr = std::unique_ptr<Expr>;
struct Expr {
std::variant<Literal, Add, Mul> tag;
std::vector<ExprPtr> children; // Add/Mul 用 children[0], children[1]
};
double eval(const Expr& e) {
return std::visit([&](const auto& tag) -> double {
using T = std::decay_t<decltype(tag)>;
if constexpr (std::is_same_v<T, Literal>) return tag.value;
else if constexpr (std::is_same_v<T, Add>)
return eval(*e.children[0]) + eval(*e.children[1]);
else
return eval(*e.children[0]) * eval(*e.children[1]);
}, e.tag);
}
更地道的写法是把 Add/Mul 做成带 ExprPtr 子结点的 struct,再 variant<Literal, AddNode, MulNode>,避免旁路 children 向量。
求值 / 打印 / 优化 pass = 不同的 visit 访问者(前一篇主线);结点形状 = 本篇的 variant 建模。
6. 能沾边,但别硬套模式名
下面这些可以用 variant 表达,但核心价值仍是 sum type 建模,不必为了「设计模式」而设计模式。
| 模式 | 和 variant 的关系 | 说明 |
|---|---|---|
| 简单工厂 | 弱 | 根据标签 emplace 不同 alternative,例如 variant<Circle, Rectangle> 由枚举驱动构造 |
| 组合(Composite) | 中 | 树结点 variant<Leaf, Branch>,Branch 里再持子结点列表 |
| 空对象 / 未初始化 | 中 | variant<monostate, T> 或 monostate 作第一个 alternative(前一篇 §3.2) |
| 备忘录(Memento) | 弱 | 快照类型各异时可 variant<SnapA, SnapB>,工程里不常见 |
// Composite 示意:叶子 vs 分支(递归类型需 unique_ptr 打破不完整类型)
struct Leaf { int value; };
struct Branch;
using Expr = std::variant<Leaf, std::unique_ptr<Branch>>;
struct Branch { char op; std::vector<Expr> kids; };
记住:能写不代表该写——若只有一种形态带数据,用 struct + optional 往往更简单。
7. 与 optional / expected 的组合
7.1 不要无脑套娃
| 需求 | 更清晰的做法 |
|---|---|
| 「可能没有值」 | std::optional<T> |
| 「成功或错误」 | std::expected<T, E>(C++23) |
| 「尚未初始化,之后必是 A/B/C 之一」 | variant<monostate, A, B, C> 或 optional<variant<A,B,C>> |
| 「成功多种形态之一,或失败」 | expected<variant<A,B>, E> |
optional<variant<...>> 多一层间接;若「空」与「多种有效形态」互斥,monostate 作第一个 alternative 往往更统一(前一篇 §3.2)。
7.2 解析管道示例(示意)
// 解析一行配置:key=value 或 key:error
using Value = std::variant<int, std::string>;
using ParseResult = std::expected<Value, std::string>;
ParseResult parse_value(std::string_view token);
// 调用方:
// if (r) std::visit(use_value, *r); else log(r.error());
8. 异构容器:vector<variant<...>>
闭集类型可以值语义放进同一容器,无需公共基类:
using Shape = std::variant<Circle, Rectangle, int>; // int 可表示「标签占位」等
std::vector<Shape> shapes;
shapes.emplace_back(Circle{1.0});
shapes.emplace_back(Rectangle{2, 3});
for (auto& s : shapes)
std::cout << std::visit(AreaVisitor{}, s) << '\n';
这与 crtp.md §8 的 方案 B 是同一思路:CRTP 接不住 vector 混放不同 Derived;闭集时用 variant 是值语义、常无堆分配的首选之一。
| 方案 | 类型集 | 容器元素 | 与 CRTP 关系 |
|---|---|---|---|
| CRTP | 编译期逐个已知 | 不能混放 Base<A> / Base<B> | 不接异构容器 |
variant + visit | 闭集 | variant<A,B,...> 内联 | 异构的互补方案 |
虚函数 + unique_ptr<Base> | 开放 | 指针多态 | 异构的经典 OO 方案 |
9. C++26 模式匹配(前瞻)
标准正在推进 inspect / match 一类语法,目标之一是对 variant 做声明式分支(类似 visit 的语法糖)。在 C++26 全面落地前,生产代码仍以:
std::visit+overload/ 具名 Visitor- 或谨慎使用
index()+switch
为主。新语法普及后,语义不变:仍是「对联合类型做穷尽分派」,只是写法更接近模式匹配。
10. 何时不要用 variant 硬撑
- 类型集开放(插件、跨 DLL ABI)→ 虚接口 / 类型擦除(见 crtp.md)。
- alternative 极多且极稀疏(如 Unicode 码点作 key)→ 内存浪费,考虑
map或专用结构。 - 需要稳定二进制布局跨版本 →
variant大小与顺序与编译器/类型集绑定,不适合随意演进 ABI。 - 递归类型未用
unique_ptr打破环 → 无法定义无限大的variant<Expr, ...>值对象。
11. 两篇文档怎么读
variant_visit.md ← Visitor 意图、visit/overload、monostate、常见坑
↓
variant_extensions.md(本文)← State/Command/ADT、optional/expected、异构 vector、选型
↓
crtp.md §8(可选) ← 与虚函数、类型擦除放在一张大表里对照
12. 一句话总结
variant首先是 sum type,不只服务于 Visitor;State / Strategy / Command / ADT 都是同一设施的常见落点。visit负责「操作」;赋值 / emplace 负责「变成哪一种」。- 与
optional/expected组合时,避免无意义套娃,优先monostate或expected<variant<...>, E>。 - 异构
vector是闭集场景下的强项,与 CRTP 互补而非替代。 - 开放扩展仍归虚函数或类型擦除,不要指望
variant万能。
记住:前一篇学「怎么 visit」;本篇学「variant 还能用来建模什么」。
更多推荐


所有评论(0)