C++17 详解 18 — std::variant

本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。

翻译太特么累人了……剩余部分还是只做摘要翻译吧。

7. std::variant

variant 是一种类型安全的 union。

头文件:

1
#include <variant>

综合应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Chapter Variant/variant_demo.cpp
#include <string>
#include <iostream>
#include <variant>

using namespace std;

// used to print the currently active type
struct PrintVisitor
{
void operator()(int i) { cout << "int: " << i << '\n'; }
void operator()(float f) { cout << "float: " << f << '\n'; }
void operator()(const string& s) { cout << "str: " << s << '\n'; }
};

int main()
{
variant<int, float, string> intFloatString;
static_assert(variant_size_v<decltype(intFloatString)> == 3);

// 默认以第一个选项进行默认初始化,值为 0。
visit(PrintVisitor{}, intFloatString);

// index 指示当前所用类型
cout << "index = " << intFloatString.index() << endl;
intFloatString = 100.0f;
cout << "index = " << intFloatString.index() << endl;
intFloatString = "hello super world";
cout << "index = " << intFloatString.index() << endl;

// try with get_if:
if (const auto intPtr = get_if<int>(&intFloatString))
cout << "int: " << *intPtr << '\n';
else if (const auto floatPtr = get_if<float>(&intFloatString))
cout << "float: " << *floatPtr << '\n';

if (holds_alternative<int>(intFloatString))
cout << "the variant holds an int!\n";
else if (holds_alternative<float>(intFloatString))
cout << "the variant holds a float\n";
else if (holds_alternative<string>(intFloatString))
cout << "the variant holds a string\n";
// try/catch and bad_variant_access
try
{
auto f = get<float>(intFloatString);
cout << "float! " << f << '\n';
}
catch (bad_variant_access&)
{
cout << "our variant doesn't hold float at this moment...\n";
}
}

输出:

1
2
3
4
5
6
int: 0
index = 0
index = 1
index = 2
the variant holds a string
our variant doesn't hold float at this moment...

示例有几个点值得注意:

  • 如果没有进行值初始化,variant 使用第一个类型初始化。此时,第一个类型必须存在默认构造函数。
  • index() 返回当前使用的类型的索引;bool holds_alternative 函数检查当前使用类型是否是 Type。
  • get<Index>get<Type> 返回当前索引/使用类型的值,如果当前使用类型和 Index/Type 不匹配,会抛出 bad_variant_access 异常。
  • get_if<Index>get_if<Type> 返回当前索引/使用类型的变量的指针,如果当前使用类型和 Index/Type 不匹配,返回 nullptr。
  • 可以配合 std::visit 使用。

7.1 创建

  1. std::in_place_index、std::in_place_type 显式赋值
1
2
variant<vector<int>, string> v{ std::in_place_index<0>, { 0, 1, 2, 3 } };
variant<vector<int>, string> v{ std::in_place_type<int>, { 0, 1, 2, 3 } };

显式指定当前索引/类型,并使用后续参数进行原地构造。

  1. std::monostate 选项
1
variant<monostate, Type-Without-Default-Constructor> v;

monostate 是 STL 提供的一个辅助类,通常用于 variant 第一个类型,可以解决第一个选项没有默认构造函数导致 variant 变量不能默认构造的问题。

monostate 可以出现在任何位置,也可以存在多个,但是违背本意。

7.2 修改

  • emplace<Index>emplace<Type>,支持基于索引/类型的原地构造。

7.3 访问存储值

  1. get、get_if 都不是 variant 的成员函数
  1. variant 上的访问者(visitor)

同 std::variant 一起引入的还有一个好用的 STL 函数:std::visit。

它可以对所有传入的 variant 调用一个 “访问者”。

声明如下:

1
2
template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variant&&... vars);

visit 会对 variant 当前使用类型调用 vis。

如果只传入一个 variant,你必须重载 variant 里所有类型(的 operator())。如果传入两个 variant,你必须重载这两个 variant 所有可能的类型组合。

访问者是“一个可调用对象,它接受每一个 variant 里每一种可能的选项”。

泛型 lambda 实现的访问者示例:

1
2
3
4
5
// 泛型 lambda:
auto PrintVisitor = [](const auto& t) { std::cout << t << '\n'; };

std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(PrintVisitor, intFloatString);

访问者同样可以修改 variant 的值,只需将访问者参数改为非常量引用。

大多数情况下,我们会希望对不同类型执行不同的动作,这时候就需要重载 operator()。

2.1 overload 模板类实现

1
2
3
4
// overload 类模板声明
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
// 前文提到过的推断指引
template<class... Ts> overload(Ts...) -> overload<Ts...>;

2.2 std::visit 中使用 overload

1
2
3
4
5
6
7
8
9
std::variant<int, float, std::string> myVariant;
std::visit(
overload {
[](const int& i) { std::cout << "int: " << i; },
[](const std::string& s) { std::cout << "string: " << s; },
[](const float& f) { std::cout << "float: " << f; }
},
myVariant
);

访问多个 variant

std::visit 允许传入多个 variant,表现是依序分别将每个 variant 中的当前类型作为访问者函数的参数。这就要求访问者必须能处理所有可能的类型组合。

比如:

1
2
std::variant<int, float, char> v1 { 's' };
std::variant<int, float, char> v2 { 10 };

可以通过提供 9 个重载实现访问者:

1
2
3
4
5
6
7
8
9
10
11
std::visit(overload{
[](int a, int b) { },
[](int a, float b) { },
[](int a, char b) { },
[](float a, int b) { },
[](float a, float b) { },
[](float a, char b) { },
[](char a, int b) { },
[](char a, float b) { },
[](char a, char b) { }
}, v1, v2);

可以添加泛型 lambda 版本的重载批量处理一些无效组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::variant<Pizza, Chocolate, Salami, IceCream> firstIngredient { IceCream() };
std::variant<Pizza, Chocolate, Salami, IceCream> secondIngredient { Chocolate()};

std::visit(overload{
[](const Pizza& p, const Salami& s) {
std::cout << "here you have, Pizza with Salami!\n";
},
[](const Salami& s, const Pizza& p) {
std::cout << "here you have, Pizza with Salami!\n";
},
[](const Chocolate& c, const IceCream& i) {
std::cout << "Chocolate with IceCream!\n";
},
[](const IceCream& i, const Chocolate& c) {
std::cout << "IceCream with a bit of Chocolate!\n";
},
[](const auto& a, const auto& b) {
std::cout << "invalid composition...\n";
},
}, firstIngredient, secondIngredient);

7.4 其它操作

  1. 比较

如果当前使用类型相同,使用相应类型的比较操作符比较;

如果当前类型不相同,以 index() 进行比较。

  1. variant 是值类型,可以移动。
  1. 可以对 std::variant 执行 std::hash。

7.5 性能 & 内存考量

  1. 同 optional 一样,variant 以尺寸最大的选项确定自身占用空间,另外有一个标识当前索引的变量。因为总是相邻存在,这个额外的标识有可能影响 CPU cache。
  1. 同 optional 一样,variant 不会动态分配内存。

评论