上一篇 variant_visit.md访问者(Visitor) 为主线,讲清 variant + visit 如何表达闭集联合并做类型安全分派。
不必把 variant 理解成「只为访问者服务」。 它本质是 闭集联合类型(sum type / tagged union):在类型里写死「当前只能是 A、B、C 之一」。很多场景里都有「多种形态、同一槽位」——variant 往往比继承 + 虚函数更直接。
本文是续篇:按与 variant 贴得有多紧梳理 State / Strategy / Command / ADT 等用法,并点到 Factory、Composite 等弱相关模式;再衔接 optional / expected、异构 vectorcrtp.md 的选型对照。


1. 先分清两件事

问题典型 API本文 / 前一篇
当前是哪一种?(数据建模)std::variant<A,B,C>emplaceholds_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
  • 执行 = visitif 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 组合时,避免无意义套娃,优先 monostateexpected<variant<...>, E>
  • 异构 vector 是闭集场景下的强项,与 CRTP 互补而非替代
  • 开放扩展仍归虚函数或类型擦除,不要指望 variant 万能。

记住:前一篇学「怎么 visit」;本篇学「variant 还能用来建模什么」。

更多推荐