std::variant
使用场景
在C++17标准出现之前,有一些神奇的术语,像"discriminated union",“type-safe union"和"sum type”。后来它们都意味着相同的类型:“variant”
std::variant
使用场景
- 所有可能为单个变量获得几种类型的地方:
- 解析命令行
- 解析ini文件
- 语言解析器
- 求解方程的根(有效地表达计算的几种可能结果)
- 错误处理:
- 例如,您可以返回
variant<Object, ErrorCode>
,如果返回值是有效的,则返回Object
,否则分配一些错误码(C++23可以使用std::expected
)。 - 实际上 optional也是可以的
- 例如,您可以返回
- 状态机
- 不使用虚表和继承实现的多态(visiting pattern)
std::variant<>
是一个可变参数类模板(C++11引入的处理任意数量参数的特性)。引入它的同时,还定义了下面的类型和对象:
- 类模板 std::variant_size
- 类模板 std::variant_alternative
- 值 std::variant_npos
- 类型 std::monostate
- 异常类 std::bad_variant_access,派生自 std::exception
对比union
头铁 手动管理union
如果你头铁,真的想手动管理union:
#include <variant>
#include <iostream>
#include <string>
#include <vector>union gUnion
{std::string str;std::vector<int> vec;~gUnion() { } // what to delete here?
};int main()
{// 初始化 Union 为一个其中一个复杂类型// - 此时,将 Union 视作其它类型(例如调用 Union.vec)是未定义行为gUnion unionObj = {"Hello, world"};std::cout << "unionObj.str = " << unionObj.str << '\n';// - 不更改激活类型的话,倒是可以连续使用unionObj.str = "OK, world";std::cout << "unionObj.str = " << unionObj.str << '\n';// 现在,把 Union 更改为其它类型// - 调用原类型对应成员的析构函数destructorunionObj.str.~basic_string<char>();// - 调用新类型的constructornew (&unionObj.vec) std::vector<int>;// 现在,新的类型视为被激活// now, s.vec is the active member of the unionunionObj.vec = {1, 2, 3, 4};std::cout << " vector 大小 = " << unionObj.vec.size() << '\n';for (auto mInt : unionObj.vec) {std::cout << mInt << ' ';}// - 使用结束后,记得调用此时类型的析构函数unionObj.vec.~vector<int>();return 0;
}
union 缺点
union 处理一些底层的逻辑,并且只使用基本类型
缺陷
- 无法获知当前使用的是哪个类型
- 它不会主动调用underlying types的析构函数
缺陷体现在使用上:需要程序员进行大量的维护存储。在切换到新的存储对象之前,程序员必须:
-
知道(/记住,因为没办法查询)当前使用的是那种类型
-
手动调用其析构函数/构造函数。
因此不推荐将 union 用于类型双关,union存储类型主要还是用于基本类型,不会看到“高级”类型,例如vector、string或者其他的容器等。
variant 优点
variant 解决了union的两个缺点:
- 能清楚的知道当前激活的类型——可以保证类型安全,不允许获取未激活类型的值
- 对复杂类型的生命周期的支持:
- 如果切换类型,会调用对应的析构函数。
- variant会自动调用非平凡类型的析构函数和构造函数。
体现在使用上:可以使用复杂类型,但不能保存引用、数组和void类型。
variant 内存大小/性能
内存大小:variant所占的内存大小等于所有可能的底层类型中最大的再加上一个记录当前选项的固定内存开销
-
遵守对齐规则
-
多出来的内存:用于保证类型安全
- 类型安全–不允许获取未激活类型的值
- 例如,需要知道当前variant中激活的类型
-
多分配的内存在栈上。variant不会要求额外的堆内存分配。
会创建在自己独立的内存空间内存储有当前选项的值的新对象。
运行时性能:拷贝 std::variant<>的开销要比拷贝当前选项的开销稍微大一点,因为 variant必须找出要拷贝哪个值。
variant对象是值语义,即拷贝被实现为深拷贝。
派生类
variant构建方式
避免报错 monostate
variant没有空的状态。每一个构造好的variant对象,至少调用了一次构造函数。
-
variant默认构造函数会调用第一个选项类型的
默认构造函数
进行初始化 -
若第一个类型没有
默认构造函数
,初始化失败 导致编译期错误。
// Iris_param类没有默认构造函数,报错
std::variant<Iris_param, std::string> iris_variant;
// pass
std::variant<std::monostate, Iris_param, std::string> iris_variant;
推荐做法:将std::monostate
作为variant的第一个类型来使用。
std::monostate
是 C++标准库提供了一个特殊的辅助类,以防止variant中的第一个类型没有默认构造函数- variant 处于这个选项时表示此 variant没有其他任何类型的值。
- 比较
std::monostate
类型的对象,结果总是相等
指定类型
通过自动推导
如果仅通过编译器帮助 variant 进行匹配(可能存在歧义)
// 自动推导类型
std::variant<std::monostate, int, std::string> iris_variant {"some string"};
指定类型
通过in_place_index指定
指定具体的类型来初始化variant
// 避免被无认为是int
std::variant<long, float, std::string> longFloatString { std::in_place_index<1>, 7 };
// 复杂的构造函数传参
std::variant<std::vector<int>, std::string> vecStr { std::in_place_index<0>, { 0, 1, 2, 3 }};
为了避免存在类型匹配的歧义 ambiguity
-
使用
std::in_place_index
来显式指定要匹配的类型。 -
同时,
std::in_place_index
也允许创建复杂的类型并将参数传递给该类型对应的构造函数。(类似 std::optional使用的 std::in_place_t)
通过in_place_type指定
std::in_place_type
指明类型
std::variant<std::vector<int>, std::string> vecStr { std::in_place_type<std::vector<int>>, { 0, 1, 2, 3 }};
variant自我包含
骚操作:
struct VariantObject;using VariantList = std::vector<VariantObject>;
using VariantDict = std::unordered_map<std::string, VariantObject>;struct VariantObject {std::variant<std::monostate, int, VariantList> inner1;std::variant<std::monostate, int, VariantDict> inner2;std::variant<std::nullptr_t, int, VariantDict, VariantList> inner3;
}
操作
当前类型
操作符 | 效果 |
---|---|
index() | 返回当前选项的索引 |
holds_alternative<T>() | 返回是否持有类型 T的值 |
获取 index()
运行时,获取当前使用的类型的索引(第一项的索引是 0)
std::variant<int, float, std::string> intFloatString { "Hello" };// 2
std::cout << intFloatString.index() << "\n";
std::variant.index()
- 通过返回的
int index
,得知当前什么类型处于激活状态。
检查 std::holds_alternative
运行时,检查当前使用的类型
if (std::holds_alternative<std::monostate>(iris))std::cout << "the variant holds a std::monostate\n";
else if (std::holds_alternative<float>(intFloatString))std::cout << "the variant holds a float\n";
else if (std::holds_alternative<std::string>(intFloatString))std::cout << "the variant holds a string\n"; // 在C++20中 可以在常量表达式中使用std::variant。
constexpr auto dv = foo(1);
static_assert(std::holds_alternative<double>(dv));
访问值
操作符 | 效果 |
---|---|
= | 赋新值 |
get<T>() | 返回类型为 T的选项的值 |
get<Idx>() | 返回索引为 Idx的选项的值 |
get_if<T>() | 返回指向类型为 T的选项的指针或 nullptr |
get_if<Idx>() | 返回指向索引为 Idx的选项的指针或 nullpr |
无法直接访问
std::variant<int, float, std::string> intFloatString { "Hello" };
// 即使已经明确知道当前激活类型是什么,也无法直接访问
std::string s = intFloatString;
// error: conversion from
// 'std::variant<int, float, std::string>'
// to non-scalar type 'std::string' requested
访问varient内部的值,需要使用如下方法:
激活状态(可以传递类型或索引)
类型安全–不允许获取未激活类型的值
std::get | std::get_if | |
---|---|---|
类型 值std::get<float>(intFloatString) | 类型index 值引用std::get_if<1>(&intFloatString) | |
激活状态 | 返回引用 | 返回指针 |
非激活状态 | 异常std::bad_variant_access | 返回**nullptr ** |
std::get
使用 std::get<Type|Index>(std::variant)
,返回 引用
所需类型状态 | 返回值 |
---|---|
激活状态 | 返回对应类型的引用 |
非激活状态 | 抛出std::bad_variant_access 异常 |
// 激活状态 int
std::variant<int, float, std::string> intFloatString { std::in_place_index<0>, 2};
try
{auto f1 = std::get<float>(intFloatString); // 通过类型访问std::cout << "float! " << f1 << "\n";auto f2 = std::get<1>(intFloatString); // 通过索引访问std::cout << "float! " << f2 << "\n";
}
catch (std::bad_variant_access&) // 当索引/类型错误时进行处 理
{std::cout << "our variant doesn't hold float at this moment...\n";
}
std::get_if
函数std::get_if<index>(&ref)
,
在当前选项为指定类型时返回一个指向当前选项的指针,否则返回 nullptr。
所需类型状态 | 返回值 |
---|---|
激活状态 | 返回对应类型的指针 |
非激活状态 | 返回nullptr (不会引发异常) |
注意:std::get
返回对variant的引用,std::get_if
返回对variant的指针。
// 激活状态 int
std::variant<int, float, std::string> intFloatString { std::in_place_index<0>, 2};if (const auto intPtr = std::get_if<0>(&intFloatString)) std::cout << "int!" << *intPtr << "\n";
std::visitor
实践中,访问variant中的值的最重要方法是使用访问者来实现。(见后文)
修改
修改值
操作符 | 效果 |
---|---|
= | 赋新值 |
get<T>() | 返回类型为 T的选项的值 |
get<Idx>() | 返回索引为 Idx的选项的值 |
get_if<T>() | 返回指向类型为 T的选项的指针或 nullptr |
get_if<Idx>() | 返回指向索引为 Idx的选项的指针或 nullpr |
operator=
赋值操作符虽然不能访问varient,但是可以修改varient
// 激活状态是 string
std::variant<int, float, std::string> intFloatString { "Hello" };
// 赋值操作符
// - 类型 修改为 int, string的析构函数被调用
intFloatString = 10; // we're now an int
std::get
std::get_if
std::get
获得引用,std::get_if
获得指针。
然后通过引用/指针给当前激活的类型赋新值
// 实际上,这让我对 对象生存期 以及 内存分布 产生了困惑。
// 各个类型的引用似乎可以同时存在
std::variant<int, float, std::string> intFloatString;
intFloatString = 1;
auto mRef_int = std::get<int>(intFloatString);
intFloatString = "Hello ";
std::get<std::string>(intFloatString) += std::string(" World");
auto mRef_str = std::get<std::string>(intFloatString);
std::cout << mRef_str << std::endl; // Hello World
std::cout << mRef_int << std::endl; // 1if (auto pFloat = std::get_if<float>(&intFloatString); pFloat)*pFloat *= 2.0f;
std::visitor访问器
修改类型/值
operator=将会直接赋予 variant一个新值,只要有和新值类型对应的选项。
emplace()在赋予新值之前会先销毁旧的值。
emplace<index/T>
修改类型/值
成员函数 | 效果 |
---|---|
emplace<T>() | 销毁旧值并赋一个 T类型选项的新值 |
emplace<Idx>() | 销毁旧值并赋一个索引为 Idx的选项的新值 |
std::variant.emplace<>{}
intFloatString.emplace<1>(float(10.22));
auto mRef_float = std::get<float>(intFloatString);
std::cout << " after emplace index1-float " << mRef_float << std::endl;
访问器 std::visitor
stl 函数std::visit
。可以在所有的variant参数上调用给定的“访问者”。
std::visit
会 “接受每个variant中所有可能的类型的可调用对象”,可调用对象必须对所有variant中所有可能的类型对进行重载。当这些对象“访问”一个 variant时,它们会调用和当前选项类型最匹配的函数。
template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
一般来说
-
静态多态:泛型lambda简单粗暴的执行统一的操作。
-
动态多态:仿函数里面重载多个
operator ()
。
静态多态(泛型lambda)
使用visitor,若采用 泛型 lambda。可以实现静态多态 —— 对variant各个类型执行相同的逻辑
值得注意的是:传入 lambda的参数不是 variant变量
,而是 variant变量中的变量
(至于是 值 引用 右值引用 常量引用… 要看具体情况)。 因此无法使用variant 的判断类型的方法 variant.index()
或std::holds_alternative
。
对于一个普通的变量,因为类型萃取发生在编译期,所以仅仅是静态多态。
通用的操作符
通过variant中的 所有类型 都支持的运算符、函数,来使用传入的变量。
// a generic lambda:
// 由于variant中的所有类型都支持<<,可以成功打印
auto PrintVisitor = [](const auto& t) {std::cout << t << "\n";
};std::variant<int, float, std::string> intFloatString { "Hello" };std::visit(PrintVisitor, intFloatString);
// 当然,若不加const,visitor可以更改variant当前的值
auto TwiceMoreVisitor = [](auto& t) {t*= 2;
};std::variant<int, float> intFloat { 20.4f };std::visit(TwiceMoreVisitor, intFloat); // 修改为40.8
std::visit(PrintVisitor, intFloat); // 打印40.8
类型萃取 + 编译期 if
通过类型萃取 + 可以使用编译期 if语句来对不同的选项类型进行不同的处理。可以根据variant中的类型来指向不同的行为。
值得注意的是:类型萃取发生在 编译期,属于静态多态。
std::variant<std::monostate, int, float, std::string> intFloatStr {std::in_place_index<2>, 20.4f
};auto TypeTraitVisitor = [](auto& arg) {using T = std::decay_t<decltype(arg)>;if (std::is_same_v<T, int>) {std::cout << "int\n";// 使用部分类型特有的 函数/运算符 需要 if constexpr} else if constexpr (std::is_same_v<T, float>) {std::cout << "float\n";arg *= 2;} else {std::cout << "other than int/float type\n";}
};std::visit(TypeTraitVisitor, intFloatStr);
动态多态(仿函数)
lambda 可以实现 执行相同的逻辑/静态多态。但在大多数情况下,我们希望根据激活类型的不同,来执行一些不同的相应操作。并且是运行时的动态多态。
在仿函数里面重载多个operator ()
,包含每个variant中所有可能的类型。仿函数/仿函数对象都是ok的
符合 “接受每个variant中所有可能的类型的可调用对象” 的定义
-
允许variant存在
std::monostate
,仿函数包含对应的重载operator()即可。 -
可以包含多余的
operator ()
重载(该variant没有的类型)。
struct MultiplyVisitor
{float mFactor = 9.9;MultiplyVisitor(float factor) : mFactor(factor) { }MultiplyVisitor() {}void operator()(std::monostate t) const {std::cout << "std::monostate type, mFactor = " << mFactor << std::endl;}void operator()(std::string& ) const {std::cout << "string called , mFactor = " << mFactor << std::endl;}void operator()(int& i) const {std::cout << "int called , mFactor = " << mFactor << std::endl;i *= static_cast<int>(mFactor);}void operator()(float& f) const {std::cout << "float called , mFactor = " << mFactor << std::endl;f *= mFactor;}
};
std::variant<std::monostate, int, float, std::string> intFloatStr {std::in_place_index<2>, 20.4f
};// 重载 operator() 的仿函数
std::visit(MultiplyVisitor(), intFloatStr);
// 只要仿函数重载了对应的operator(),monostate也是允许的
intFloatStr = std::monostate();
std::visit(MultiplyVisitor(0.5f), intFloatStr);// 也可以是仿函数对象
intFloatStr = "string sth";
MultiplyVisitor mVisitor = MultiplyVisitor(99.0f);std::visit(mVisitor, intFloatStr);
异常造成的无值
赋新值给variant时发生异常,variant可能会进入特殊的状态:它已经失去了旧的值但还没有获得新的值。
struct S {operator int() { throw "EXCEPTION"; } // 转换为int时会抛出异常
};
std::variant<double, int> var{12.2};
// OOPS:当设为int时抛出异常
var.emplace <1>(S{});
如果这种情况发生了,一下函数可以表示 variant当前没有值 的状态:
var.valueless_by_exception()
返回 truevar.index()
返回 std::variant_npos