不能完全替代,但能安全覆盖绝大多数场景;std::variant通过运行时类型索引保障安全,而union无类型信息,易致未定义行为。

std::variant 能否完全替代 union?
不能直接替代,但能安全覆盖 union 的绝大多数使用场景。union 本身不记录当前存储的类型,std::variant 在运行时维护一个类型索引(tag),每次访问前强制检查——这是类型安全的核心代价和保障。
常见错误现象:std::get<int>(v)</int> 在 v 实际存的是 double 时抛出 std::bad_variant_access;而裸 union 强转会静默读错内存,引发未定义行为。
- 适用场景:需要在几个固定类型间切换、且逻辑上“非此即彼”的数据容器(如配置项、AST节点、协议字段)
- 不适用场景:需要极低内存开销(
std::variant至少多占 1–2 字节 tag)、或需与 C ABI 二进制兼容(比如共享内存结构体) - 性能影响:一次访问多一次 tag 比较,现代 CPU 分支预测下开销极小;但频繁切换类型可能影响缓存局部性
如何安全访问 std::variant 中的值?
别用 std::get<t></t> 直接强取,它只适合你 100% 确定当前类型时——这在真实逻辑中极少成立。优先用 std::visit,它天然强制处理所有可能分支。
示例:假设 std::variant<int std::string double> v = "hello";</int>
立即学习“C++免费学习笔记(深入)”;
std::visit([](const auto& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << x;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << x;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << x;
}
}, v);- 用
std::holds_alternative<t>(v)</t>做运行时类型判断,再配合std::get<t></t>——仅当分支逻辑复杂、无法塞进 lambda 时考虑 - 避免写
std::get(v)这类序号访问:一旦 variant 模板参数顺序调整,代码就崩,且完全失去类型语义 - 如果漏掉某个备选类型(比如没处理
double),编译器不会报错,但运行时遇到该类型会调用默认的std::terminate
std::variant 和 std::monostate 配合解决“空状态”问题
原始 std::variant 不允许为空,但很多场景需要表达“尚未初始化”或“无效值”。此时加 std::monostate 是最轻量、最标准的做法。
例如:std::variant<:monostate int std::string> maybe_value;</:monostate>,初始值就是 std::monostate。
- 不要用
std::optional<:variant>></:variant>:多一层间接,且std::monostate本身零大小、零开销 - 访问前必须先判断:
if (std::holds_alternative<:monostate>(maybe_value)) { /* 未设置 */ }</:monostate> - 注意:C++17 的
std::variant构造函数默认初始化第一个类型(如int会被初始化为 0),加std::monostate到首位才能确保默认为空
移动语义和异常安全的关键细节
std::variant 的赋值和构造默认是强异常安全的,但前提是所含类型的移动/拷贝操作本身不抛异常。一旦某个备选类型移动构造可能抛异常,整个 variant 的赋值就可能中途失败并回滚——这点比裸 union 复杂得多。
- 若所有备选类型都满足
noexcept移动(如内置类型、std::string在 C++11 后通常满足),则std::variant的移动也是noexcept - 否则,用
v.emplace<t>(args...)</t>替代赋值,它绕过旧值析构,直接就地构造新值,避免中间状态异常 - 容易被忽略的点:
std::variant的operator=是“赋值后保证有效”,但不保证原值不变——旧值可能已被移动走,别假设它还能读








