std::variant是C++17引入的类型安全联合体,解决传统union不安全(无类型记录、未定义行为)和不支持复杂类型(如std::string)的问题;它编译期限定类型列表、运行时记录活跃类型、自动管理构造/析构,并提供std::get、std::holds_alternative和std::visit等安全访问机制。

std::variant 是什么,它解决什么问题
当你需要一个变量能存几种不同类型(比如 int、std::string、double),又不想用 void* 或裸联合体(union)手动管理类型和生命周期时,std::variant 就是标准答案。它本质是一个类型安全的并集(tagged union):内部自动记录当前存储的是哪种类型,并在访问时做运行时检查。
注意:它不是 std::any——后者只存“任意类型”,不预设类型列表;而 std::variant 必须在编译期明确列出所有可能类型,例如 std::variant<int std::string bool></int>。
std::get 和 std::get 的区别与误用风险
std::get 有两种重载形式,容易混淆:
-
std::get<t>(v)</t>:按类型获取。如果v当前不持有T,抛出std::bad_variant_access -
std::get<i>(v)</i>:按索引获取(I是编译期整型常量,从 0 开始)。如果I超出变体类型列表长度,编译失败;但若索引合法而当前值不是该位置的类型,同样抛异常
常见错误:
立即学习“C++免费学习笔记(深入)”;
- 对含重复类型的
std::variant(如std::variant<int int std::string></int>)用std::get<int></int>—— 编译失败(类型不唯一) - 没检查
v.index()或没用std::holds_alternative<t>(v)</t>就直接std::get<t>(v)</t>—— 运行时崩溃 - 把
std::get(v)当作“取第一个值”用,却忽略索引对应的是声明顺序,不是值插入顺序
安全访问 std::variant 的三种常用方式
别裸写 std::get,尤其在线上代码里。
推荐组合:
- 先用
std::holds_alternative<t>(v)</t>判断是否持有类型T,再调用std::get<t>(v)</t> - 用
std::visit+ lambda 处理所有分支,天然覆盖所有情况,且无法遗漏类型 - 对简单场景,可用
v.index()切换,但要注意:索引值依赖模板参数顺序,重构类型列表时会悄悄改变语义
示例(推荐 std::visit):
std::variant<int, std::string, double> v = "hello";
std::visit([](const auto& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << x << "\n";
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << x << "\n";
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << x << "\n";
}
}, v);
std::variant 的移动语义和异常安全性细节
std::variant 默认构造时处于“值初始化”状态(即第一个类型的默认构造),但你不能假设它总是安全可读——未初始化的 std::variant(比如只声明未赋值)是未定义行为。
关键点:
- 赋值或 emplace 后,旧值会被正确析构(前提是该类型析构函数不抛异常)
- 若某备选类型的构造函数/赋值运算符抛异常,
std::variant保证自身处于有效但未指定状态(v.valueless_by_exception() == true),此时任何std::get都会抛std::bad_variant_access - 不要对
valueless_by_exception()状态的 variant 做任何访问操作,必须先重置(如v.emplace(42))
容易被忽略的是:即使你只用了 int 和 std::string,只要其中一个类型构造可能抛异常(比如 std::string 分配失败),整个 variant 的赋值就不再是 noexcept。















