相关系列文章
目录
2.3 std::variant_size和std::variant_size_v
2.4 std::variant_alternative和std::variant_alternative_t
1简介
C++17的三剑客分别是std::optional, std::any, std::vairant。今天主要讲std::variant。std::variant的定义如下:
- template< class... Types >
- class variant;
类模板 std::variant
表示一个类型安全的联合体(以下称“变化体”)。std::variant
的一个实例在任意时刻要么保有它的一个可选类型之一的值,要么在错误情况下无值(此状态难以达成,见 valueless_by_exception);从功能上讲,它就跟union的功能差不多,但却比union更高级;variant主要是为了提供更安全的union。举个例子union里面不能有string这种类型,但std::variant却可以,还可以支持更多复杂类型,如map等
2辅助类
2.1 std::monostate
为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造。
- std::variant
int> test; // OK - std::cout << "index: " << test.index(); //输出: 0
我们可以简单理解std::monostate是个占位符。
2.2 std::bad_variant_access
std::bad_variant_access
是下列情形中抛出的异常类型:
- 以不匹配当前活跃可选项的下标或类型调用 std::get(std::variant)
- 调用 std::visit 观览因异常无值 (valueless_by_exception) 的
variant
示例如下:
- #include
- #include
-
- int main()
- {
- std::variant<int, float> v;
- v = 12;
- try {
- std::get<float>(v);
- }
- catch(const std::bad_variant_access& e) {
- std::cout << e.what() << '\n';
- }
- }
可能的输出:
bad_variant_access
2.3 std::variant_size和std::variant_size_v
提供作为编译时常量表达式,对可有 cv 限定的 variant
中可选项数量的访问,示例如下:
- #include
- #include
- #include
-
- static_assert(std::variant_size_v
> == 0); - static_assert(std::variant_size_v
int>> == 1); - static_assert(std::variant_size_v
int, int>> == 2); - static_assert(std::variant_size_v
int, int, int>> == 3); - static_assert(std::variant_size_v
int, float, double>> == 3); - static_assert(std::variant_size_v
void>> == 2); - static_assert(std::variant_size_v
const int, const float>> == 2); - static_assert(std::variant_size_v
>> == 1); -
- int main() { std::puts("All static assertions passed."); }
输出:
All static assertions passed.
2.4 std::variant_alternative和std::variant_alternative_t
提供对 variant
的类型编译时下标访问,示例如下:
- #include
- #include
-
- using my_variant = std::variant<int, float>;
- static_assert(std::is_same_v
- <int, std::variant_alternative_t<0, my_variant>>);
- static_assert(std::is_same_v
- <float, std::variant_alternative_t<1, my_variant>>);
- // variant 类型上的 cv 限定传播给提取出的可选项类型。
- static_assert(std::is_same_v
- <const int, std::variant_alternative_t<0, const my_variant>>);
-
- int main()
- {
- std::cout << "All static assertions passed\n";
- }
输出:
All static assertions passed
3 std::variant的操作
std::variant提供的主要操作有:
操作 | 说明 |
---|---|
constructors | 创建一个variant对象(可能调用底层类型的构造函数) |
destructor | 销毁一个variant对象 |
emplace | 为具有类型T的备选项分配一个新值 |
emplace | 为索引Idx的备选项分配一个新值 |
= | 分配一个新值 |
index() | 返回当前备选项的索引 |
holds_alternative | 返回类型T是否有值 |
==, !=, <, <=, >, >= | 比较variant对象 |
swap() | 交换两个对象的值 |
hash<> | 函数对象类型来计算哈希值 |
valueless_by_exception() | 返回该变量是否由于异常而没有值 |
get | 返回备选项类型为T的值或抛出异常(如果没有类型为T的值) |
get | 返回备选项索引为idx的值或抛出异常(如果没有索引为idx的值) |
get_if | 返回指向类型为T指针或返回nullptr(如果没有类型为T的值) |
get_if | 返回指向索引Idx的指针或nullpt(如果没有索引为idx的值) |
visit() | 为当前备选项执行操作 |
3.1 定义
直接定义std::variant,如:
std::variantuint32_t, double, std::string, std::int32_t> t;
可以对 t 赋初值,对于基本类型,它是0、false还是nullptr。如果传递一个值进行初始化,则使用最佳匹配类型,如:
- std::variant
uint32_t, double, std::string, std::int32_t> t{ 25 }; - cout << t.index(); //输出: 3
要传递多个值进行初始化,必须使用in_place_type或in_place_index标记:
- std::variant
double>> t1{1.0,564.0}; // ERROR - std::variant
double>> t2{std::in_place_typedouble>>, - 322.0, 2323.0};
- std::variant
double>> t3{std::in_place_index<0>, 35.0, 8.0};
如果初始化过程中出现歧义或匹配问题,可以用in_place_index标签来解决,如:
- std::variant<int, int> t1{std::in_place_index<1>, 77}; // init 2nd int
- std::variant<int, long> t2{std::in_place_index<1>, 77}; // init long, not int
- std::cout << t2.index(); // prints 1
总结:不同构造方式的适用场景和优缺点
构造方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
直接赋值构造 | 类型明确,无歧义时 | 简单直观 | 当多个类型可从相同构造参数构造时可能引起歧义 |
std::in_place_type | 需要明确指定 std::variant 存储的类型 | 避免歧义,明确类型 | 略显繁琐,需要写出具体类型 |
默认构造函数 | 当第一个类型可默认构造时 | 简单,不需要提供初始值 | 仅适用于第一个类型可默认构造的场景 |
拷贝构造和移动构造 | 从另一个 std::variant 对象复制或移动内容 | 适用于对象的复制和移动 | 可能涉及性能开销(尤其是拷贝构造) |
std::in_place_index | 当需要构造 std::variant 中特定索引的类型时 | 可以精确地选择要构造的类型 | 需要了解类型的确切索引 |
使用单一值初始化 | 类型明确,且由该值可以唯一确定 std::variant 类型 | 简单,直接 | 如有多个可能的类型匹配,可能导致歧义 |
聚合初始化 | 初始化复合类型(如结构体) | 适合复杂或结构化的数据 | 仅限于聚合类型 |
通过这个表格表格,我们可以更清晰地看到每种 std::variant
构造方式的应用场景和各自的优缺点。这种多角度的分析有助于在实际编程中做出更合适的选择,从而提高代码的效率和可读性。
注意:std::variant不允许保存引用、数组或void类型(references, arrays, or the type void);空的std::variant也是格式错误的。
3.2 访问值
std::variant可以通std::get来获取或修改值。
- std::variant
uint32_t, double, std::string, std::int32_t> var; - auto x = std::get<double>(var);
- auto y = std::get<4>(var); // compile-time ERROR: no 4th alternative
- auto c = std::get<int>(var);
-
- try{
- auto m = std::get
(var); // throws exception (first int currently set) - auto n = std::get<0>(var); // OK, i==0
- auto o = std::get<1>(var); // throws exception (other int currently set)
- }
- catch (const std::bad_variant_access& e) { // in case of an invalid access
- std::cout << "Exception: " << e.what() << '\n';
- }
std::get_if用来判断某项是否存在
- #include
- #include
-
- int main()
- {
- std::variant<int, float> v{12};
-
- if(auto pval = std::get_if<int>(&v))
- std::cout << "variant value: " << *pval << '\n'; //输出: 12
- else
- std::cout << "failed to get value!" << '\n';
- }
还可以通过std::visit来访问值:
- struct stValueVisitor {
- void operator()(int i) { cout << "int: " << i << '\n'; }
- void operator()(float f) { cout << "float: " << f << '\n'; }
- void operator()(const std::string& s) { cout << "str: " << s << '\n'; }
- };
-
- int main() {
- std::variant<int, float, string> value = 65.2;
- std::visit(stValueVisitor{}, value); // 输出 float: 65.2
- return 0;
- }
也可以利用C++17 新增的 overloaded 模板,可以直接生成匿名访问器,简化代码, 下面的代码是等价的:
- int main() {
- std::variant<int, float, string> t= 56.4;
- std::visit(overloaded{
- void operator()(int i) { cout << "int: " << i << '\n'; }
- void operator()(float f) { cout << "float: " << f << '\n'; }
- void operator()(const std::string& s) { cout << "str: " << s << '\n'; }
- },t);
- }
接下来还有第三种方法来访问std::variant :
- int main()
- {
- std::variant<int, float, string> t = 16.4;
- std::visit(
- [&](auto &&arg) {
- using T = std::decay_t<decltype(arg)>; // 类型退化,去掉类型中的const 以及 &
- if constexpr(std::is_same_v
int>) { - cout << "int: " << arg << '\n';
- } else if constexpr(std::is_same_v
float>){ - cout<< "float: "<< arg <<'\n';
- } else if constexpr(std::is_same_v
){ - cout<< "str: "<< arg <<'\n';
- }
- }, t);
- }
这里我们可以看出来第三种写法比第一种的优势在哪里了:编译期推断。
第三种方法由于使用了constexpr 进行 if 分支的判断,因此是在编译期运行,而第一种方法是运行期进行类型判断,效率是不同的。
第二种方法和模板一样,也是编译期推断的,因此效率也是很高的,所以我们应当尽量使用 std::visit 方法来访问variant 变量
另外std::visit 还有一个好处是,它的参数列表是不定长的,我们可以传入多个variant 变量
- template <class Visitor, class... Variants>
- constexpr visit(Visitor&& vis, Variant&&... vars);
3.3 修改值
用std::get来修改值,如:
- std::variant
uint32_t, double, std::string, std::int32_t> t; - std::get<double>(t) = 1.0;
- cout << t.index(); //输出: 1
- std::get
uint32_t>(t) = 34; - cout << t.index(); //输出: 0
- std::get<2>(t) = "4343636";
- cout << t.index(); //输出:2
也可以用std::get_if来修改值,如:
- std::variant
uint32_t, double, std::string, std::int32_t> t; - if (auto p = std::get_if<1>(&t)) { // if second int set
- *p = 42.2; // modify it
- }
4 总结
以上都是我日常工作中对std::variant的用法的总结;做技术,要知其然,更要知其所以然,后面我将从std::varant的源码实现上继续分析它的原理,敬请期待。。。
C++三剑客之std::variant(二):原理探究-CSDN博客
评论记录:
回复评论: