在 C++17 之前,处理不同类型的对象常用的方式是使用继承体系和虚函数,或者使用 std::any、boost::variant 等第三方库。然而这两种方法都存在一定的缺陷:继承体系需要提前定义所有派生类,且在运行时才可确定具体类型;std::any 的使用会导致类型擦除,使用者必须自己手动转型,容易出错。C++17 标准库提供了 std::variant,它是一种类型安全的多态容器,既可以在编译期确定所有可能的类型,又能在运行时安全地访问其中的值。
下面从定义、使用、访问、组合等方面,系统讲解 std::variant 的用法。
1. 基本概念
std::variant<Types...> v;
Types...是一系列不同类型。std::variant 只允许其中一个类型处于激活状态。- 在任何时候,variant 至少保持一个类型(默认构造时为第一个类型)处于激活状态。
2. 创建和初始化
// 只激活第一个类型(int)
std::variant<int, std::string, double> v1;
// 直接初始化为 std::string
std::variant<int, std::string, double> v2{std::string("hello")};
// 直接初始化为 double
std::variant<int, std::string, double> v3{3.14};
注意:如果想显式指定激活的类型,应使用 std::in_place_type_t 或 std::in_place_index_t:
std::variant<int, std::string, double> v4{std::in_place_type<std::string>, "world"};
3. 访问值
3.1 通过 std::get
int i = std::get <int>(v1); // 正确,激活的是 int
std::string s = std::get<std::string>(v2); // 正确
如果访问的类型不匹配,会抛出 std::bad_variant_access。
3.2 通过 std::get_if
if (auto p = std::get_if <double>(&v1)) {
std::cout << "double: " << *p << '\n';
}
std::get_if 在类型不匹配时返回 nullptr,避免异常。
3.3 访问当前激活的类型
std::cout << "index: " << v1.index() << '\n';
std::cout << "type: " << v1.type().name() << '\n';
index()返回激活类型在类型列表中的索引(从 0 开始)。type()返回std::type_info对象,使用name()可以获取编译器实现的类型名称(不一定可读)。
4. 访问与转换
4.1 std::visit
最强大的访问方式是 std::visit,它采用访问者模式,可以对所有可能的类型统一处理:
std::visit([](auto&& arg) {
std::cout << "value: " << arg << '\n';
}, v2);
如果想让访问者知道具体类型,可以使用重载:
std::visit(overloaded{
[](int i){ std::cout << "int: " << i << '\n'; },
[](const std::string& s){ std::cout << "string: " << s << '\n'; },
[](double d){ std::cout << "double: " << d << '\n'; }
}, v2);
这里 overloaded 是一个帮助结构体模板,用于合并多种 lambda 以实现重载:
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
4.2 类型推导
如果想在访问时获得激活类型,可以使用 std::variant_alternative_t:
using T = std::variant_alternative_t< v2.index(), decltype(v2) >;
T value = std::get <T>(v2);
5. 组合与嵌套
std::variant 可以嵌套,形成更复杂的数据结构。例如:
using IntOrString = std::variant<int, std::string>;
using Nested = std::variant<int, IntOrString, double>;
Nested n = IntOrString{42}; // 激活 IntOrString,内部激活 int
在访问嵌套时,需要先确定外层类型,再确定内层:
std::visit([](auto&& outer){
std::visit([](auto&& inner){
std::cout << inner << '\n';
}, outer);
}, n);
6. 常见错误与调试技巧
- 错误 1:访问未激活的类型导致异常。使用
std::get_if或std::visit的重载来避免。 - 错误 2:忘记显式构造
std::variant,导致默认构造为第一个类型。必要时使用std::in_place_type。 - 错误 3:类型列表包含相同类型,编译错误。确保所有类型唯一。
7. 性能与空间占用
std::variant 的内部实现通常是一个联合(union)加上一个表示当前类型的索引。空间占用是所有候选类型中最大的那一个,且不需要额外的堆分配。访问 std::get 或 std::visit 的成本与类型数量无关,通常只有一次索引比较和一次跳转。
8. 实际案例:实现一个简易 JSON 值
using JsonValue = std::variant<
std::nullptr_t,
bool,
int64_t,
double,
std::string,
std::vector <JsonValue>,
std::map<std::string, JsonValue>
>;
这样就可以用递归方式表示 JSON 的各种数据结构,而不必写繁琐的继承体系。
9. 小结
std::variant提供了 类型安全 的多态容器,兼顾编译期类型检查与运行时动态选择。- 通过
std::get、std::get_if、std::visit等接口,可以灵活安全地访问其中的值。 - 结合
overloaded与std::visit,可以写出清晰、可维护的访问代码。 - 对于嵌套、组合数据结构,
std::variant与std::vector、std::map等容器结合,可实现复杂的 DSL、配置文件或消息系统。
掌握 std::variant 的用法后,你会发现许多传统继承+虚函数的需求,可以被更简洁、更安全的代码所取代。