std::variant
std::variant 是 C++17 引入的 类型安全联合(type-safe union)。在任一时刻,variant<...> 要么保存其候选类型列表中的某一类型的对象(active alternative),要么在异常情况等导致的特殊情形下处于无值状态(valueless_by_exception())。
头文件:
#include <variant>
1. 模板定义
template< class... Types >
class variant;
- 模板参数为
Types...:每个T必须满足 Destructible(能被析构)。 - 不允许持有引用类型、数组类型或
void。 - 可以重复出现相同类型(例如
variant<int,int>合法),也可以出现不同 cv 限定的同一基础类型(如int与const int)。 - 注意:如果你用同一个具体类型多次,基于类型的访问(
std::get<T>/get_if<T>)会变得歧义 / 编译失败(只能用索引或明确in_place_type/in_place_index)。 - 默认构造:默认构造会构造第一个候选类型的默认值,如果第一个候选类型不可默认构造,则
variant本身也不可默认构造。可以把std::monostate放在首位以保证可默认构造。
2. 存储与对象布局
variant内部存储了 discriminator(索引)与一个能容纳最大候选类型的缓冲区;当variant持有某个类型T时,一个T对象会嵌套(placement-new)在该缓冲区内。- 因此
variant的大小≈(max sizeof(alternatives))+ 对齐 + discriminator 大小。 - 在异常情况下(构造/赋值期间)有可能变为 valueless_by_exception(见下文)。
3. 主要成员函数 / 重载
下面列出常用操作、签名(伪签名风格)与行为说明与例子。
构造与析构
-
variant()- 默认构造:构造第一个候选类型的默认值(若可行)。
- 否则
variant不可默认构造。
-
variant(const variant&)/variant(variant&&)- 拷贝 / 移动构造。条件:候选类型支持相应操作;若某些候选类型不可拷贝/移动,相应操作会被删除。
- 如果在移动过程中抛出异常,可能导致
valueless_by_exception(取决于具体实现与异常传播)。
-
converting constructors(从某个值构造)
- 如果传入
U可明确/唯一地构造某个候选类型,variant会构造该候选。若存在二义性(能构造多个候选),编译失败。
- 如果传入
-
in-place 构造(直接在 variant 内就地构造)
variant(in_place_type<T>, Args&&...); variant(in_place_index<I>, Args&&...);in_place_type_t/in_place_index_t用于在 variant 内直接构造目标 alternative,避免先创建临时再赋值。
-
~variant()- 默认析构:会调用当前活动 alternative 的析构函数(如果有值)。
赋值(operator=)
-
variant& operator=(const variant&); -
variant& operator=(variant&&);- 这两个做拷贝/移动赋值。赋值行为在不同情况下(同类型 index / 不同 index)会调用相应 alternative 的赋值/析构+构造。
- 赋值过程中若抛异常,可能使
variant进入valueless_by_exception。
-
template<class T> variant& operator=(T&&);- converting assignment:当
T可以唯一构造某个候选类型时,执行相应赋值/替换。
- converting assignment:当
-
variant& operator=(std::monostate)等(视候选类型而定)。
观察器(Observers)
-
std::size_t index() const noexcept;- 返回当前活动的候选类型的零基索引(0..N-1)。
- 如果处于
valueless_by_exception,返回variant_npos(常为std::size_t(-1))。
-
bool valueless_by_exception() const noexcept;- 如果
variant处于无值状态(例如在变更 active alternative 时异常导致)返回true。
- 如果
修改(Modifiers)
-
template<class T, class... Args> T& emplace(Args&&...);emplace<T>(args...):在variant中就地构造类型T(T 必须是某个 alternative);会销毁旧的 active 值(若有),然后就地构造新值。- 异常安全:如果构造抛出,
variant可能进入valueless_by_exception(取决于实现与被替换对象的销毁时机)。
-
template<size_t I, class... Args> variant& emplace(in_place_index_t<I>, Args&&...);- 使用索引 I 就地构造。
-
void swap(variant& other) noexcept( /* depends */ );- 交换两个 variant 的状态与内容。noexcept 与具体候选类型的 swap/移动操作相关。
访问(get / get_if)
-
std::get<T>(variant&)/std::get<I>(variant&)get<T>(按类型访问)要求T在候选类型中唯一,否则编译错误。get<I>(按索引访问)直接访问索引为I的候选类型。- 若
variant当前不保存请求的 alternative,std::get会 抛出std::bad_variant_access(运行时异常)。
-
std::get_if<T>(&variant)/std::get_if<I>(&variant)get_if返回指向当前值的指针(非nullptr表示匹配),失败时返回nullptr。不会抛异常,通常是更安全的访问方式。- 有
const/ 非const重载:const T* get_if<const T>(&const variant)等。
示例:
std::variant<int,std::string> v = "hi";
if (auto p = std::get_if<std::string>(&v)) {
std::cout << *p << "\n";
}
try {
std::cout << std::get<int>(v); // 抛出 std::bad_variant_access
} catch (const std::bad_variant_access& e) { ... }
访问辅助:std::holds_alternative<T>(v)
- 返回
true当且仅当v当前持有类型T(同get_if<T>非空)。T必须唯一出现在 alternatives 中。
访问索引常量
-
constexpr std::size_t variant_npos = /* often size_t(-1) */;- 表示无效索引(用于
index()返回值在valueless_by_exception()时)。
- 表示无效索引(用于
4. 访问与遍历:std::visit(Visitor 模式)
非成员 std::visit(自 C++17 起)
签名(概念):
template <class Visitor, class... Variants>
decltype(auto) visit(Visitor&& vis, Variants&&... vars);
std::visit会将 visitor(可调用对象)以当前 variant(或多个 variants)的活动值作为参数调用。- 当传入多个
variant时,visitor 会被调用,参数顺序与variant顺序对应。 - 如果任一
variant为valueless_by_exception(),std::visit通常会抛出std::bad_variant_access。 - 返回值类型由 visitor 决定(可以返回
void或其他类型)。 - 常用技巧:用
overloaded(多个 lambda 继承合并)来实现多分支处理:
// helper
template<class... Fs> struct overloaded : Fs... { using Fs::operator()...; };
template<class... Fs> overloaded(Fs...) -> overloaded<Fs...>;
// 使用
std::variant<int,std::string> v = 42;
std::visit(overloaded {
[](int i){ std::cout<<"int "<<i<<"\n"; },
[](const std::string& s){ std::cout<<"str "<<s<<"\n"; }
}, v);
成员 visit(C++26 提案:member visit)
- C++26 引入(或将引入)
v.visit(visitor)的成员形式作为便捷写法(请注意你使用的编译器/标准支持情况)。非成员std::visit在 C++17 就有。
5. 比较运算与哈希
-
operator==等(C++17 起)与operator<=>(C++20)有定义:通常两个variant先比较是否都valueless_by_exception(),再比较index(),在 index 相同时比较包含的值(按对应类型的比较运算)。==:若两者index()相同且 contained values 相等 => true;若两个都 valueless => true;否则 false。</>:若index()不同,通常以index()的大小决定排序;若相同,则调用 contained type 的<。- 详细边界(valueless 等)以标准详细定义为准,但通常结果符合“按 index 首先排序,然后按值比较”的直觉。
-
std::hash<std::variant<...>>在标准库有特化(要求所有候选类型可哈希)。
6. 辅助类型与特性(type traits / helper classes)
-
std::monostate(C++17)- 一个空占位类型,常用于将
variant设置为默认可构造:std::variant<std::monostate, T1, T2>。
- 一个空占位类型,常用于将
-
std::bad_variant_access(C++17)- 当用
std::get<T>/std::get<I>访问但variant未持有该 alternative 时抛出。
- 当用
-
std::variant_size<Variant>/std::variant_size_v<Variant>(C++17)- 编译期获取候选类型数量(常量表达式)。
- 例:
std::variant_size_v<std::variant<int,double>> == 2。
-
std::variant_alternative<I, Variant>::type/std::variant_alternative_t<I, Variant>(C++17)- 编译期获取索引
I对应的类型(类型别名)。 - 例:
std::variant_alternative_t<0,std::variant<int,double>>等于int。
- 编译期获取索引
-
variant_npos:表示无值索引(如index()在 valueless 时返回此值)。
7. 异常安全与 valueless_by_exception
-
在某些变更 active alternative 的操作中(例如赋值、就地构造时),如果构造/移动/复制新的 alternative 的构造函数抛出异常,而旧对象已被销毁,
variant可能无法恢复到原先状态,从而进入valueless_by_exception()。 -
一旦处于
valueless_by_exception():index()返回variant_npos;std::get抛出std::bad_variant_access;std::get_if返回nullptr;- 一些操作(比如
std::visit)会抛出bad_variant_access(取决于实现)。
-
预防策略:当替换可能抛异常的类型时,优先使用
emplace并在必要时进行异常处理;确保候选类型的构造/移动操作尽可能noexcept,可以降低进入无值状态的风险。
8. 常用例子
基本使用与 get/get_if/holds_alternative
std::variant<int,std::string> v = "hello";
if (std::holds_alternative<std::string>(v)) {
std::cout << std::get<std::string>(v) << "\n";
}
if (auto p = std::get_if<int>(&v)) {
std::cout << "int: " << *p << "\n";
} else {
std::cout << "not int\n";
}
emplace / in_place
std::variant<std::monostate, std::string, std::vector<int>> v;
v.emplace<std::string>("abc"); // 就地构造 std::string
v.emplace<std::vector<int>>(3, 42); // 就地构造 vector(3,42)
v.emplace<in_place_index_t<1>>("xyz"); // 使用索引就地构造(index=1 => std::string)
visit 与 overloaded 工具
auto handle = overloaded {
[](int i){ std::cout<<"int "<<i<<"\n"; },
[](const std::string& s){ std::cout<<"str "<<s<<"\n"; }
};
std::variant<int,std::string> v = 10;
std::visit(handle, v);
使用 monostate 使可默认构造
std::variant<std::monostate, std::string> v; // 默认构造后 v 持有 monostate
9. 实用建议 / 常见误用
- 不要把
variant作为替代所有情况:类型过多会导致代码复杂和 visitor 分支膨胀。若候选类型集合非常大或松散,考虑设计别的抽象(多态/策略等)。 - 当候选类型有重复的具体类型时,避免
get<T>:因为会编译错误;使用get<index>或in_place_type显式选择。 - 注意异常安全:替换 active alternative(赋值、emplace)如果构造抛异常,可能进入
valueless_by_exception;为关键路径确保候选类型的移动/复制构造尽可能noexcept。 - 避免把对
variant的访问当作频繁反射:大量类型判断/切换会影响可读性和性能(虽然variant本质上是常数时间的判定与访问,但分支与 visitor 的实现复杂度需考虑)。 - std::variant 类似于 Rust 的 enum,都能表示“一种类型中的多种可能”。 不同点在于Rust 的 enum 语法更简洁,且模式匹配是强制的;C++ 的 std::variant 需要 std::visit 或 get 来显式处理。
10. 标准/特性备注
std::variant自 C++17 引入(特性宏:__cpp_lib_variant等)。- 标准后续对
variant做过修订(例如std::visit扩展、constexpr 能力增强等)。例如有成员形式visit(C++26 提议/扩展),以及使variant更多操作支持constexpr(不同标准版本的支持程度由编译器/标准库实现决定)。 - 使用时注意你的编译器和标准库版本对
variant的各项特性的支持情况(尤其是constexpr、成员visit等较新特性)。
11. 快速 API 参考
- 头文件:
<variant> - 构造:
variant(),variant(in_place_type_t<T>, ...),variant(in_place_index_t<I>, ...), converting constructors - 赋值:
operator=(variant),operator=(T&&)(converting) - 访问:
std::get<T>(v),std::get<I>(v),std::get_if<T>(&v),std::get_if<I>(&v) - 情况检测:
v.index(),v.valueless_by_exception(),std::holds_alternative<T>(v) - 就地构造:
v.emplace<T>(args...),v.emplace<in_place_index_t<I>>(args...) - 访问模式:
std::visit(visitor, v1, v2, ...),C++26 可能支持成员v.visit(visitor) - 辅助类型:
std::monostate,std::bad_variant_access,std::variant_size,std::variant_alternative_t,std::hash<std::variant<...>>