解决什么问题?
// ❌ 传统 union 的问题
union Value {
int i;
double d;
const char* s;
};
Value v;
v.i = 42; // 现在 v 存的是 int
v.d = 3.14; // 现在 v 存的是 double,覆盖了 int
// 编译器不管!v.i 现在是垃圾数据,但代码能访问 v.i → 未定义行为!
// 你必须自己用 tag 记录当前类型,但很容易忘记
variant 的承诺:编译期记住当前是哪种类型,访问错了会报错/抛异常。
什么时候用?
| 场景 | 为什么选 variant |
|---|---|
| 函数返回"要么是正常结果,要么是错误信息" | 替代 pair<bool, T> 或 -1 这样的哨兵值 |
| 解析器(JSON、INI、YAML)的节点类型 | JSON 值可以是 string/number/array/object |
| 状态机 | 状态是多种不同的结构体,但同一时刻只有一种 |
| 返回异构类型 | 函数可能返回 int、double 或 string |
三种访问方式(选哪种?)
std::variant<int, double, std::string> v = 42;
// 方式 1:get<T>——你知道当前是什么类型时用
int val = std::get<int>(v); // ✅ 正确
double val2 = std::get<double>(v); // ❌ 抛 bad_variant_access
// 方式 2:get_if<T>——不确定时用,安全
if (auto* p = std::get_if<double>(&v)) {
// p 是 double*,当前 v 存的是 double 时才进入
} else if (auto* p = std::get_if<int>(&v)) {
// 当前 v 存的是 int
}
// 方式 3:visit——最推荐,"我不管是什么类型,你帮我处理每个类型"
std::visit([](const auto& val) {
// 编译器会为 int/double/string 各生成一个 lambda 实例
// val 在三个实例中分别是 int、double、string
std::cout << val;
}, v);
我推荐 visit:因为你不写大量 if-else,不容易漏掉某个类型。而且 visit 是编译期展开的,没有运行时成本。
真实案例:配置文件解析
using ConfigValue = std::variant<int, double, std::string, bool>;
std::unordered_map<std::string, ConfigValue> config;
config["timeout"] = 30;
config["rate"] = 3.14;
config["name"] = "server";
config["enabled"] = true;
// 安全获取配置
template <typename T>
T get_config(const std::string& key, T default_val) {
auto it = config.find(key);
if (it == config.end()) return default_val;
if (auto* p = std::get_if<T>(&it->second)) return *p;
return default_val;
}
int timeout = get_config<int>("timeout", 10); // 30
double rate = get_config<double>("rate", 1.0); // 3.14
bool enabled = get_config<bool>("enabled", false); // true