类模板std::variant
表示一个类型安全的联合体(以下称“变化体”)。std::visit
可以处理std::variant
中的值。它最大的优势是提供了一种新的具有多态性的处理异质集合的方法。也就是说,它可以帮助我们处理不同类型的数据,并且不需要公共基类和指针。
起源于C
语言,C++
也提供对union
的支持,它的作用是持有一个值,这个值的类型可能是指定的若干类型中的任意 一个 。然而,这项语言特性有一些缺陷:
std::string
(没有进行特殊处理的话)。union
派生。示例
#include
#include
#include
union S {
std::string str;
std::vector<int> vec;
~S() { } // what to delete here?
};
int main()
{
S s = {"Hello, world"};
// 此时,从s.vec读取是未定义的行为
std::cout << "s.str = " << s.str << '\n';
// 你必须调用所包含对象的析构函数!
s.str.~basic_string<char>();
// 还有一个构造函数!
new (&s.vec) std::vector<int>;
// 现在,s.vec是union的活跃成员
s.vec.push_back(10);
std::cout << s.vec.size() << '\n';
// 另一个析构函数
s.vec.~vector<int>();
}
通过std::variant<>
,C++
标准库提供了一种 可辨识的联合(closed discriminated union) (这意味着要指明一个可能的类型列表)
事实上,一个std::variant<>
持有的值有若干 候选项(alternative) ,这些选项通常有不同的类型。然而,两个不同选项的类型也有可能相同,这在多个类型相同的选项分别代表不同含义的时候很有用(例如,可能有两个选项类型都是字符串,分别代表数据库中不同列的名称,你可以知道当前的值代表哪一个列)。
variant
所占的内存大小等于所有可能的底层类型中最大的再加上一个记录当前选项的固定内存开销。不会分配堆内存。
一般情况下,除非你指定了一个候选项来表示为空,否则variant
不可能为空。然而,在非常罕见的情况下(例如赋予一个不同类型新值时发生了异常),variant
可能会变为没有值的状态。
和std::optional<>
、std::any
一样,variant
对象是值语义。也就是说,拷贝被实现为 深拷贝 ,将会创建一个在自己独立的内存空间内存储有当前选项的值的新对象。然而,拷贝std::variant<>
的开销要比拷贝当前选项的开销稍微大一点,这是因为variant
必须找出要拷贝哪个值。另外,variant
也支持move
语义。
示例:
#include
#include
#include
using namespace std;
struct SampleVisitor {
void operator()(int i) const { cout << "int: " << i << "\n"; }
void operator()(float f) const { cout << "float: " << f << "\n"; }
void operator()(const string& s) const { cout << "string: " << s << "\n"; }
};
int main() {
variant<int, float, string> intFloatString;
static_assert(variant_size_v<decltype(intFloatString)> == 3);
// 默认初始化为第一个选项,应该是0
visit(SampleVisitor{}, intFloatString);
// 索引将显示当前使用的 'type'
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)); intPtr)
cout << "int!" << *intPtr << "\n";
else if (const auto floatPtr(get_if<float>(&intFloatString)); floatPtr)
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";
}
// visit:
visit(SampleVisitor{}, intFloatString);
intFloatString = 10;
visit(SampleVisitor{}, intFloatString);
intFloatString = 10.0f;
visit(SampleVisitor{}, intFloatString);
}
运行结果如下:
int: 0
index = 0
index = 1
index = 2
the variant holds a string
our variant doesn't hold float at this moment...
string: hello super world
int: 10
float: 10
上面的例子中展示了几件事:
index()
或holds_alternative
来了解当前使用的类型。get_if
或get
来访问该值(但这可能会抛出bad_variant_access
异常)。variant
类调用非平凡类型的析构函数和构造函数,因此在本例中,字符串对象在切换到新的变体之前被清理。下面的代码展示了std::variant<>
的核心功能:
#include
#include
int main()
{
std::variant<int, std::string> var{"hi"}; // 初始化为string选项
std::cout << var.index() << '\n'; // 打印出1
var = 42; // 现在持有int选项
std::cout << var.index() << '\n'; // 打印出0
...
try {
int i = std::get<0>(var); // 通过索引访问
std::string s = std::get<std::string>(var); // 通过类型访问(这里会抛出异常)
...
}
catch (const std::bad_variant_access& e) { // 当索引/类型错误时进行处理
std::cerr << "EXCEPTION: " << e.what() << '\n';
...
}
}
成员函数index()
可以用来指出当前选项的索引(第一个选项的索引是0)。
初始化和赋值操作都会查找最匹配的选项。如果类型不能精确匹配,可能会发生奇怪的事情。
注意空variant
、有引用成员的variant
、有C
风格数组成员的variant
、有不完全类型(例如void
)的variant
都是不允许的。(variant
不能保有引用、数组,或类型 void
。空variant
也非良构(可用 std::variantstd::monostate 代替) )
variant
没有空的状态。这意味着每一个构造好的variant
对象,至少调用了一次构造函数。默认构造函数会调用第一个选项类型的默认构造函数:
std::variant<std::string, int> var; // => var.index()==0, 值==""
如果第一个类型没有默认构造函数,那么调用variant
的默认构造函数将会导致编译期错误:
struct NoDefConstr {
NoDefConstr(int i) {
std::cout << "NoDefConstr::NoDefConstr(int) called\n";
}
};
std::variant<NoDefConstr, int> v1; // ERROR:不能默认构造第一个选项
辅助类型std::monostate
提供了处理这种情况的能力,还可以用来模拟空值的状态。
std::monostate
占位符类型有意为行为良好的
std::variant
中空可选项所用的单位类型。具体而言,非可默认构造的variant
可以列std::monostate
为其首个可选项:这使得variant
自身可默认构造。
为了支持第一个类型没有默认构造函数的variant
,C++
标准库提供了一个特殊的辅助类:std::monostate
。
std::monostate
类型的对象总是处于相同的状态。因此,比较它们的结果总是相等。它的作用是可以作为一个选项,当variant
处于这个选项时表示此variant 没有其他任何类型的值 。
因此,std::monostate
可以作为第一个选项类型来保证variant
能默认构造。例如:
std::variant<std::monostate, NoDefConstr, int> v2; // OK
std::cout << "index: " << v2.index() << '\n'; // 打印出0
某种意义上,你可以把这种状态解释为variant
为空的信号。
下面的代码展示了几种检测monostate
的方法,也同时展示了variant
的其他一些操作:
if (v2.index() == 0) {
std::cout << "has monostate\n";
}
if (!v2.index()) {
std::cout << "has monostate\n";
}
if (std::holds_alternative<std::monostate>(v2)) {
std::cout << "has monostate\n";
}
if (std::get_if<0>(&v2)) {
std::cout << "has monostate\n";
}
if (std::get_if<std::monostate>(&v2)) {
std::cout << "has monostate\n";
}
get_if<>()
的参数是一个指针,并在当前选项为T
时返回一个指向当前选项的指针,否则返回nullptr
。这和get
不同,后者获取variant
的引用作为参数并在提供的索引或类型正确时以值返回当前选项,否则抛出异常。和往常一样,你可以赋予variant
一个和当前选项类型不同的其他选项的值,甚至可以赋值为monostate
来表示为空:
v2 = 42;
std::cout << "index: " << v2.index() << '\n'; // index:2
v2 = std::monostate{};
std::cout << "index: " << v2.index() << '\n'; // index: 0
完整示例如下:
#include
#include
#include
#include
using namespace std;
struct NoDefConstr {
NoDefConstr(int i) { cout << "NoDefConstr::NoDefConstr(int) called\n"; }
};
int main() {
variant<monostate, NoDefConstr, int> v2; // OK
cout << "index: " << v2.index() << '\n'; // 打印出0
if (v2.index() == 0) {
cout << "has monostate\n";
}
if (!v2.index()) {
cout << "has monostate\n";
}
if (holds_alternative<monostate>(v2)) {
cout << "has monostate\n";
}
if (get_if<0>(&v2)) {
cout << "has monostate\n";
}
if (get_if<monostate>(&v2)) {
cout << "has monostate\n";
}
v2 = 42;
cout << "index: " << v2.index() << '\n'; // index:2
v2 = monostate{};
cout << "index: " << v2.index() << '\n'; // index: 0
}
运行结果如下:
index: 0
has monostate
has monostate
has monostate
has monostate
has monostate
index: 2
index: 0
你可以从variant
派生。例如,你可以定义如下派生自std::variant<>
的聚合体:
#include
#include
#include
#include
using namespace std;
class Derived : public std::variant<int, std::string> {};
int main() {
Derived d = {{"hello"}};
std::cout << d.index() << '\n'; // 打印出:1
std::cout << std::get<1>(d) << '\n'; // 打印出:hello
d.emplace<0>(77); // 初始化为int,销毁string
std::cout << std::get<0>(d) << '\n'; // 打印出:77
}
运行结果如下:
1
hello
77
预处理代码如下:
class Derived : public std::variant<int, std::basic_string<char> >
{
public:
// inline ~Derived() noexcept = default;
};
int main()
{
Derived d = {std::variant<int, std::basic_string<char> >{"hello"}};
std::operator<<(std::cout.operator<<(static_cast<const std::variant<int, std::basic_string<char> >&>(d).index()), '\n');
std::operator<<(std::operator<<(std::cout, std::get<1>(static_cast<std::variant<int, std::basic_string<char> >&>(d))), '\n');
static_cast<std::variant<int, std::basic_string<char> >&>(d).emplace<0>(77);
std::operator<<(std::cout.operator<<(std::get<0>(static_cast<std::variant<int, std::basic_string<char> >&>(d))), '\n');
return 0;
}
在头文件variant
,C++
标准库以如下方式定义了类std::variant<>
:
namespace std {
template<typename... Types> class variant;
// 译者注:此处原文的定义是
// template class variant;
// 应是作者笔误
}
也就是说,std::variant<>
是一个 可变参数(variadic) 类模板(C++11
引入的处理任意数量参数的特性)。
另外,还定义了下面的类型和对象:
std::variant_size
std::variant_alternative
std::variant_npos
std::monostate
std::bad_variant_access
,派生自std::exception
还有两个为variant
定义的变量模板:std::in_place_type<>
和std::in_place_index<>
。它们的类型分别是std::in_place_type_t
和std::in_place_index_t
,定义在头文件
中。
下表列出了std::variant<>
的所有操作。
操作符 | 效果 |
---|---|
构造函数 | 创建一个variant 对象(可能会调用底层类型的构造函数) |
析构函数 | 销毁一个variant 对象 |
= |
赋新值 |
emplace |
销毁旧值并赋一个T 类型选项的新值 |
emplace |
销毁旧值并赋一个索引为Idx 的选项的新值 |
valueless_by_exception() |
返回变量是否因为异常而没有值 |
index() |
返回当前选项的索引 |
swap() |
交换两个对象的值 |
==、!=、<、<=、>、>= |
比较variant对象 |
hash<> |
计算哈希值的函数对象类型 |
holds_alternative |
返回是否持有类型T的值 |
get |
返回类型为T 的选项的值 |
get |
返回索引为Idx 的选项的值 |
get_if |
返回指向类型为T 的选项的指针或nullptr |
get_if |
返回指向索引为Idx 的选项的指针或nullptr |
visit() |
对当前选项进行操作 |
constexpr variant() noexcept(/* see below */);
构造 variant
,保有首个可选项的值初始化的值(index()
为零)。
T_0
的值初始化满足 constexpr
函数的要求,此构造函数才为constexpr
。std::is_default_constructible_v
为 true
时才会参与重载决议。std::variant<int, std::string> var; // 值初始化第一个可选项,第一个int初始化为0,index()==0
cout<< "index " << var.index() << " ; holds_alternative = "<<holds_alternative<int>(var) <<" ; get(var) = " << std::get<int>(var)<<endl;
选项被默认初始化,意味着基本类型会初始化为0
、nullptr
。运行结果:
index 0 ; holds_alternative = 1 ; get<int>(var) = 0
constexpr variant( const variant& other );
若 other
非因异常无值,则构造一个保有与other
相同可选项的 variant
,并以std::get
直接初始化所含值。否则,初始化一个因异常无值的 variant
。
std::is_copy_constructible_v
对于所有Types...
中的T_i
为 true
。std::is_trivially_copy_constructible_v
对Types...
中的所有 T_i
为 true
则它为平凡。variant<string, int> var{"STR"};// 用 string{"STR"}; 初始化第一个可选项
cout << "var " << get<string>(var) << endl;
auto var1 (var); // 用 var初始化
cout << "var1 " << get<string>(var1) << " "<< var1.index() << endl;
constexpr variant( variant&& other ) noexcept(/* see below */);
若 other
非因异常无值,则构造一个保有与 other
相同可选项的variant
并以 std::get
直接初始化所含值。否则,初始化一个因异常无值的 variant
。
std::is_move_constructible_v
对于所有Types...
中的T_i
为 true
时才会参与重载决议。std::is_trivially_move_constructible_v
对 Types...
中的所有T_i
为 true
则它为平凡。template< class T >
constexpr variant( T&& t ) noexcept(/* see below */);
构造保有会被重载决议对表达式 F(std::forward
选择的可选项T_j
,假设对来自Types...
中的每个T_i
同时存在一个虚构函数F(T_i)
的重载,除了:
T_i x[] = { std::forward(t) };
对某个虚设变量 x
才考虑 F(T_i)
。如同用直接非列表初始化从std::forward
直接初始化所含值。
sizeof...(Types) > 0
,std::decay_t
(C++20
前)std::remove_cvref_t
(C++20
起) 既不与variant
为同一类型,亦非std::in_place_type_t
或 std::in_place_index_t
的特化,std::is_constructible_v
为 true ,F(std::forward(t))
(令 F
为上述虚构函数的重载集)为良构时才会参与重载决议。T_j
的被选择构造函数为 constexpr
构造函数,则此构造函数为constexpr
构造函数。std::variant<std::string> v("abc"); // OK
std::variant<std::string, std::string> w("abc"); // 谬构
std::variant<std::string, const char*> x("abc"); // OK :选择 const char*
std::variant<std::string, bool> y("abc"); // OK :选择 string ; bool 不是候选
std::variant<float, long, double> z = 0; // OK :保有 long
// float 与 double 不是候选
T
的 variant
并以参数std::forward(args)...
初始化所含值。template< class T, class... Args >
constexpr explicit variant( std::in_place_type_t<T>, Args&&... args );
T
的被选择构造函数是 constexpr
构造函数,则此构造函数亦为 constexpr
构造函数。Types...
中正好出现一次 T
且std::is_constructible_v
为 true
时才会参与重载决议。 T
的 variant
并以参数il, std::forward(args)...
初始化所含值。template< class T, class U, class... Args >
constexpr explicit variant( std::in_place_type_t<T>,
std::initializer_list<U> il, Args&&... args );
constexpr
构造函数。 Types...
中正好出现一次 T
且 std::is_constructible_v&, Args...>
为true
时才会参与重载决议。 I
所指定的可选项类型 T_i
的 variant
并以参数std::forward(args)...
初始化所含值。template< std::size_t I, class... Args >
constexpr explicit variant( std::in_place_index_t<I>, Args&&... args );
T_i
的被选择构造函数是 constexpr
构造函数,则此构造函数亦为 constexpr
构造函数。I < sizeof...(Types)
与std::is_constructible_v
皆为 true
时才会参与重载决议。I
所指定的可选项类型T_i
的 variant
并以参数il, std::forward(args)...
初始化所含值。template< std::size_t I, class U, class... Args >
constexpr explicit variant( std::in_place_index_t<I>,
std::initializer_list<U> il, Args&&... args );
T_i
的被选择构造函数是constexpr
构造函数,则此构造函数亦为constexpr
构造函数。 I < sizeof...(Types)
与std::is_constructible_v&, Args...>
皆为true
时才会参与重载决议。示例1 如果传递一个值来初始化,将会使用最佳匹配的类型:
std::variant<long, int> v2{42};
std::cout << v2.index() << '\n'; // 打印出1
示例2如果有两个类型同等匹配会导致歧义:
std::variant<long, long> v3{42}; // ERROR:歧义
std::variant<int, float> v4{42.3}; // ERROR:歧义
std::variant<int, double> v5{42.3}; // OK
std::variant<int, long double> v6{42.3}; // ERROR:歧义
std::variant<std::string, std::string_view> v7{"hello"}; // ERROR:歧义
std::variant<std::string, std::string_view, const char*> v8{"hello"}; // OK
std::cout << v8.index() << '\n'; // 打印出2
示例3 为了传递多个值来调用构造函数初始化,你必须使用in_place_type
或者in_place_index
标记:
std::variant<std::complex<double>> v9{3.0, 4.0}; // ERROR
std::variant<std::complex<double>> v10{{3.0, 4.0}}; // ERROR
std::variant<std::complex<double>> v11{std::in_place_type<std::complex<double>>, 3.0, 4.0};
std::variant<std::complex<double>> v12{std::in_place_index<0>, 3.0, 4.0};
你也可以使用in_place_index
在初始化时解决歧义问题或者打破匹配优先级:
std::variant<int, int> v13{std::in_place_index<1>, 77}; // 初始化第二个int
std::variant<int, long> v14{std::in_place_index<1>, 77}; // 初始化long,而不是int
std::cout << v14.index() << '\n'; // 打印出1
示例4 传递一个带有其他参数的初值列:
// 用一个lambda作为排序准则初始化一个set的variant:
auto sc = [] (int x, int y) {
return std::abs(x) < std::abs(y);
};
std::variant<std::vector<int>, std::set<int, decltype(sc)>>
v15{std::in_place_index<1>, {4, 8, -7, -2, 0, 5}, sc};
示例5 只有当所有初始值都和容器里元素类型匹配时才可以这么做。否则你必须显式传递一个std::initializer_list<>
:
// 用一个lambda作为排序准则初始化一个set的variant
auto sc = [] (int x, int y) {
return std::abs(x) < std::abs(y);
};
std::variant<std::vector<int>, std::set<int, decltype(sc)>>
v15{std::in_place_index<1>, std::initializer_list<int>{4, 5L}, sc};
示例6 std::variant<>
不支持类模板参数推导,也没有make_variant<>()
快捷函数(不像std::optional<>
和std::any
)。这样做也没有意义,因为variant
的目标是处理多个候选项。
如果所有的候选项都支持拷贝,那么就可以拷贝variant
对象:
struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy&) = delete;
};
std::variant<int, NoCopy> v1;
std::variant<int, NoCopy> v2{v1}; // ERROR
综合示例
#include
#include
#include
int main() {
// 默认构造
std::variant<int, float> intFloat;
std::cout << intFloat.index() << ", value " << std::get<int>(intFloat)
<< "\n";
// monostate for default initialization:
class NotSimple {
public:
NotSimple(int, float) {}
};
// std::variant cannotInit; // error
std::variant<std::monostate, NotSimple, int> okInit;
std::cout << okInit.index() << "\n";
// 传值:
std::variant<int, float, std::string> intFloatString{10.5f};
std::cout << intFloatString.index() << ", value "
<< std::get<float>(intFloatString) << "\n";
// 歧义
// double 可能会转换为浮点型或 int,因此编译器无法决定
// std::variant intFloatString { 10.5 };
// 使用in_place起义
std::variant<long, float, std::string> longFloatString{
std::in_place_index<1>, 7.6}; // double!
std::cout << longFloatString.index() << ", value "
<< std::get<float>(longFloatString) << "\n";
// in_place 复杂的类型
std::variant<std::vector<int>, std::string> vecStr{std::in_place_index<0>,
{0, 1, 2, 3}};
std::cout << vecStr.index() << ", vector size "
<< std::get<std::vector<int>>(vecStr).size() << "\n";
// 从其他variant复制初始化:
std::variant<int, float> intFloatSecond{intFloat};
std::cout << intFloatSecond.index() << ", value "
<< std::get<int>(intFloatSecond) << "\n";
}
运行结果如下:
ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
0, value 0
0
1, value 10.5
1, value 7.6
0, vector size 4
0, value 0
通常的方法是调用get<>()
或get_if<>
来访问当前选项的值。你可以传递一个索引、或者传递一个类型(该类型的选项只能有一个)。使用一个无效的索引和无效/歧义的类型将会导致编译错误。如果访问的索引或者类型不是当前的选项,将会抛出一个std::bad_variant_access
异常。
归结如下:
- 以不匹配当前活跃可选项的下标或类型调用
std::get(std::variant)
- 调用
std::visit
观览因异常无值(valueless_by_exception)
的variant
get()
函数template< std::size_t I, class... Types >
constexpr std::variant_alternative_t<I, std::variant<Types...>>&
get( std::variant<Types...>& v );
v.index() == I
,则返回到存储于v
的值的引用。否则抛出 std::bad_variant_access
。若I
不是 variant
的合法下标,则此调用为病式。v
保有可选项 T
,则返回到存储于v
的值的引用。否则抛出 std::bad_variant_access
。若 T
不是 Types...
中唯一存在的元素,则此调用为病式。例1:
std::variant<int, int, std::string> var; // 第一个int设为0,index()==0
auto a = std::get<double>(var); // 编译期ERROR:没有double类型的选项
auto b = std::get<4>(var); // 编译期ERROR:没有第五个选项
auto c = std::get<int>(var); // 编译期ERROR:有两个int类型的选项
try {
auto s = std::get<std::string>(var); // 抛出异常(当前选项是第一个int)
auto i = std::get<0>(var); // OK,i==0
auto j = std::get<1>(var); // 抛出异常(当前选项是另一个int)
}
catch (const std::bad_variant_access& e) {
std::cout << "Exception: " << e.what() << '\n';
}
例2:
#include
#include
#include
#include
#include
using namespace std;
int main() {
std::variant<int, float> v{12}, w;
int i = std::get<int>(v);
w = std::get<int>(v);
cout << "w = " << std::get<int>(w) << endl;
w = std::get<0>(v); // 效果同前一行
cout << "w = " << std::get<int>(w) << endl;
// std::get(v); // 错误: [int, float] 中无 double
// std::get<3>(v); // 错误:合法的 index 值是 0 和 1
try {
std::get<float>(w); // w 含有 int ,非 float :将抛出异常
} catch (std::bad_variant_access& e) {
cout << "Exception:" << e.what() << endl;
}
}
运行结果如下:
w = 12
w = 12
Exception:std::get: wrong index for variant
预处理代码如下:
#include
#include
#include
#include
#include
using namespace std;
int main()
{
std::variant<int, float> v = std::variant<int, float>{12};
std::variant<int, float> w = std::variant<int, float>();
int i = std::get<int>(v);
w.operator=(std::get<int>(v));
std::operator<<(std::cout, "w = ").operator<<(std::get<int>(w)).operator<<(std::endl);
w.operator=(std::get<0>(v));
std::operator<<(std::cout, "w = ").operator<<(std::get<int>(w)).operator<<(std::endl);
try
{
std::get<float>(w);
} catch(std::bad_variant_access & e) {
std::operator<<(std::operator<<(std::cout, "Exception:"), e.what()).operator<<(std::endl);
}
;
return 0;
}
get_if()
返回指向存储于被指向的 variant
中值的指针,错误时为空指针。template< std::size_t I, class... Types >
constexpr std::add_pointer_t<std::variant_alternative_t<I, std::variant<Types...>>>
get_if( std::variant<Types...>* pv ) noexcept;
pv
不是空指针且pv->index() == I
,则返回指向存储于 pv
所指向的 variant
中的值的指针。否则,返回空指针值。若 I
不是variant
的合法下标,则此调用良构。I
是 T
在Types...
中的零基下标。若 T
不是 Types...
中的唯一存在的元素,则此调用非良构。get_if()
可以在访问值之前先检查给定的选项是否是当前选项。
if (auto ip = std::get_if<1>(&var); ip != nullptr) {
std::cout << *ip << '\n';
}
else {
std::cout << "alternative with index 1 not set\n";
}
这里还使用了带初始化的if
语句,把初始化过程和条件检查分成了两条语句。你也可以直接把初始化语句用作条件语句:
if (auto ip = std::get_if<1>(&var)) {
std::cout << *ip << '\n';
}
else {
std::cout << "alternative with index 1 not set\n";
}
示例:
#include
#include
int main() {
std::variant<int, float> v{12};
if (auto pval = std::get_if<float>(&v))
std::cout << "variant value: " << *pval << '\n';
else
std::cout << "failed to get value!" << '\n';
}
运行结果
failed to get value!
预处理代码
#include
#include
int main()
{
std::variant<int, float> v = std::variant<int, float>{12};
{
float * pval = std::get_if<float>(&v);
if(pval) {
std::operator<<(std::operator<<(std::cout, "variant value: ").operator<<(*pval), '\n');
} else {
std::operator<<(std::operator<<(std::cout, "failed to get value!"), '\n');
}
}
return 0;
}
另一种访问不同选项的值的方法是使用variant
访问器。
赋值操作和emplace()
函数可以修改值:
std::variant<int, int, std::string> var; // 设置第一个int为0,index()==0
var = "hello"; // 设置string选项,index()==2
var.emplace<1>(42); // 设置第二个int,index()==1
预处理代码如下:
std::variant<int, int, std::basic_string<char> > var = std::variant<int, int, std::basic_string<char> >();
var.operator=("hello");
var.emplace<1>(42);
注意 operator=
将会直接赋予variant
一个新值,只要有和新值类型对应的选项。emplace()
在赋予新值之前会先销毁旧的值。
你也可以使用get<>()
或者get_if<>()
来给当前选项赋予新值:
std::variant<int, int, std::string> var; // 设置第一个int为0,index()==0
std::get<0>(var) = 77; // OK,但当前选项仍是第一个int
std::get<1>(var) = 99; // 抛出异常(因为当前选项是另一个int)
if (auto p = std::get_if<1>(&var); p) { // 如果第二个int被设置
*p = 42; // 修改值
}
std::get<1>(var) = 99;
抛出如下异常:
terminate called after throwing an instance of 'std::bad_variant_access'
what(): std::get: wrong index for variant
预处理代码如下:
std::variant<int, int, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > var = std::variant<int, int, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >();
std::get<0>(var) = 77;
{
int * p = std::get_if<1>(&var);
if(p) {
*p = 42;
}
}
另一个修改不同选项的值的方法是variant访问器。
对两个类型相同的variant
(也就是说,它们每个选项的类型和顺序都相同),你可以使用通常的比较运算符。
比较运算将遵循如下规则:
variant
当前的选项相同,将调用当前选项类型的比较运算符进行比较。std::monostate
类型的对象都相等。variant
都处于特殊状态(valueless_by_exception()
为真的状态)时相等。否则,valueless_by_exception()
返回ture
的variant
小于另一个。例如:
#include
#include
#include
int main() {
std::cout << std::boolalpha;
std::string cmp;
bool result;
auto print2 = [&cmp, &result](const auto& lhs, const auto& rhs) {
std::cout << lhs << ' ' << cmp << ' ' << rhs << " : " << result << '\n';
};
std::variant<int, std::string> v1, v2;
std::cout << "operator==\n";
{
cmp = "==";
// by default v1 = 0, v2 = 0;
result = v1 == v2; // true
std::visit(print2, v1, v2);
v1 = v2 = 1;
result = v1 == v2; // true
std::visit(print2, v1, v2);
v2 = 2;
result = v1 == v2; // false
std::visit(print2, v1, v2);
v1 = "A";
result = v1 == v2; // false: v1.index == 1, v2.index == 0
std::visit(print2, v1, v2);
v2 = "B";
result = v1 == v2; // false
std::visit(print2, v1, v2);
v2 = "A";
result = v1 == v2; // true
std::visit(print2, v1, v2);
}
std::cout << "operator<\n";
{
cmp = "<";
v1 = v2 = 1;
result = v1 < v2; // false
std::visit(print2, v1, v2);
v2 = 2;
result = v1 < v2; // true
std::visit(print2, v1, v2);
v1 = 3;
result = v1 < v2; // false
std::visit(print2, v1, v2);
v1 = "A";
v2 = 1;
result = v1 < v2; // false: v1.index == 1, v2.index == 0
std::visit(print2, v1, v2);
v1 = 1;
v2 = "A";
result = v1 < v2; // true: v1.index == 0, v2.index == 1
std::visit(print2, v1, v2);
v1 = v2 = "A";
result = v1 < v2; // false
std::visit(print2, v1, v2);
v2 = "B";
result = v1 < v2; // true
std::visit(print2, v1, v2);
v1 = "C";
result = v1 < v2; // false
std::visit(print2, v1, v2);
}
{
std::variant<int, std::string> v1;
std::variant<std::string, int> v2;
// v1 == v2; // Compilation error: no known conversion
}
}
运行结果如下:
operator==
0 == 0 : true
1 == 1 : true
1 == 2 : false
A == 2 : false
A == B : false
A == A : true
operator<
1 < 1 : false
1 < 2 : true
3 < 2 : false
A < 1 : false
1 < A : true
A < A : false
A < B : true
C < B : false
预处理代码如下:
#include
#include
#include
int main()
{
std::cout.operator<<(std::boolalpha);
std::basic_string<char> cmp = std::basic_string<char>();
bool result;
class __lambda_10_19
{
public:
template<class type_parameter_0_0, class type_parameter_0_1>
inline /*constexpr */ auto operator()(const type_parameter_0_0 & lhs, const type_parameter_0_1 & rhs) const
{
(((((((std::cout << lhs) << ' ') << cmp) << ' ') << rhs) << " : ") << result) << '\n';
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<int, int>(const int & lhs, const int & rhs) const
{
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(lhs), ' '), cmp), ' ').operator<<(rhs), " : ").operator<<(result), '\n');
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<std::basic_string<char>, std::basic_string<char> >(const std::basic_string<char> & lhs, const std::basic_string<char> & rhs) const
{
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, lhs), ' '), cmp), ' '), rhs), " : ").operator<<(result), '\n');
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<std::basic_string<char>, int>(const std::basic_string<char> & lhs, const int & rhs) const
{
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, lhs), ' '), cmp), ' ').operator<<(rhs), " : ").operator<<(result), '\n');
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<int, std::basic_string<char> >(const int & lhs, const std::basic_string<char> & rhs) const
{
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(lhs), ' '), cmp), ' '), rhs), " : ").operator<<(result), '\n');
}
#endif
private:
std::basic_string<char> & cmp;
bool & result;
public:
__lambda_10_19(std::basic_string<char> & _cmp, bool & _result)
: cmp{_cmp}
, result{_result}
{}
};
__lambda_10_19 print2 = __lambda_10_19{cmp, result};
std::variant<int, std::basic_string<char> > v1 = std::variant<int, std::basic_string<char> >();
std::variant<int, std::basic_string<char> > v2 = std::variant<int, std::basic_string<char> >();
std::operator<<(std::cout, "operator==\n");
{
cmp.operator=("==");
result = std::operator==(v1, v2);
std::visit(print2, v1, v2);
v1.operator=(v2.operator=(1));
result = std::operator==(v1, v2);
std::visit(print2, v1, v2);
v2.operator=(2);
result = std::operator==(v1, v2);
std::visit(print2, v1, v2);
v1.operator=("A");
result = std::operator==(v1, v2);
std::visit(print2, v1, v2);
v2.operator=("B");
result = std::operator==(v1, v2);
std::visit(print2, v1, v2);
v2.operator=("A");
result = std::operator==(v1, v2);
std::visit(print2, v1, v2);
};
std::operator<<(std::cout, "operator<\n");
{
cmp.operator=("<");
v1.operator=(v2.operator=(1));
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v2.operator=(2);
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v1.operator=(3);
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v1.operator=("A");
v2.operator=(1);
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v1.operator=(1);
v2.operator=("A");
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v1.operator=(v2.operator=("A"));
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v2.operator=("B");
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
v1.operator=("C");
result = std::operator<(v1, v2);
std::visit(print2, v1, v2);
};
{
std::variant<int, std::basic_string<char> > v1 = std::variant<int, std::basic_string<char> >();
std::variant<std::basic_string<char>, int> v2 = std::variant<std::basic_string<char>, int>();
};
return 0;
}
只要所有的选项都支持move
语义,那么std::variant<>
也支持move
语义。
如果你move
了variant
对象,那么当前状态会被拷贝,而当前选项的值会被move
。因此,被move
的variant
对象仍然保持之前的选项,但值会变为未定义。
你也可以把值移进或移出variant
对象。
如果所有的选项类型都能计算哈希值,那么variant
对象也能计算哈希值。注意variant
对象的哈希值 不 保证是当前选项的哈希值。在某些平台上它是,有些平台上不是。
另一个处理variant
对象的值的方法是使用访问器(visitor
)。访问器是为每一个可能的类型都提供一个函数调用运算符的对象。当这些对象“访问”一个variant
时,它们会调用和当前选项类型最匹配的函数。
例如:
#include
#include
#include
struct MyVisitor {
void operator()(int i) const { std::cout << "int: " << i << '\n'; }
void operator()(std::string s) const {
std::cout << "string: " << s << '\n';
}
void operator()(long double d) const {
std::cout << "double: " << d << '\n';
}
};
int main() {
std::variant<int, std::string, long double> var(42);
std::visit(MyVisitor(), var); // 调用int的operator()
var = "hello";
std::visit(MyVisitor(), var); // 调用string的operator()
var = 42.7;
std::visit(MyVisitor(), var); // 调用long double的operator()
}
预处理代码如下:
struct MyVisitor
{
inline void operator()(int i) const
{
std::operator<<(std::operator<<(std::cout, "int: ").operator<<(i), '\n');
}
inline void operator()(std::basic_string<char> s) const
{
std::operator<<(std::operator<<(std::operator<<(std::cout, "string: "), s), '\n');
}
inline void operator()(long double d) const
{
std::operator<<(std::operator<<(std::cout, "double: ").operator<<(d), '\n');
}
};
int main()
{
std::variant<int, std::basic_string<char>, long double> var = std::variant<int, std::basic_string<char>, long double>(42);
std::visit(MyVisitor(), var);
var.operator=("hello");
std::visit(MyVisitor(), var);
var.operator=(42.700000000000003);
std::visit(MyVisitor(), var);
return 0;
}
如果访问器没有某一个可能的类型的operator()
重载,那么调用visit()
将会导致编译期错误,如果调用有歧义的话也会导致编译期错误。
例如:std::variant
代码将无法通过编译。
....
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/variant:1032:31: error: no matching function for call to '__invoke(MyVisitor, double&)'
1032 | return std::__invoke(std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1033 | __element_by_index_or_cookie<__indices>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1034 | std::forward<_Variants>(__vars))...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
....
你也可以使用访问器来修改当前选项的值(但不能赋予一个其他选项的新值)。例如:
#include
#include
#include
using namespace std;
struct Twice {
void operator()(double& d) const { d *= 2; }
void operator()(int& i) const { i *= 2; }
void operator()(std::string& s) const { s = s + s; }
};
int main() {
std::variant<int, std::string, double> var(42);
std::visit(Twice(), var); // 调用匹配类型的operator()
cout << std::get<0>(var);
}
访问器调用时只根据类型判断,你不能对类型相同的不同选项做不同的处理。
注意上面例子中的函数调用运算符都应该标记为const
,因为它们是 无状态的(stateless) (它们的行为只受参数的影响)。
最简单的使用访问器的方式是使用泛型lambda
,它是一个可以处理任意类型的函数对象:
#include
#include
#include
using namespace std;
int main() {
auto pp = [] (const auto& val) {
std::cout << val << '\n';
};
std::variant<int> var(42);
std::visit( pp, var);
}
这里,泛型lambda
生成的闭包类型中会将函数调用运算符定义为模板:
class ComplierSpecificClosureTypeName {
public:
template<typename T>
auto operator() (const T& val) const {
std::cout << val << '\n';
}
};
因此,只要调用时生成的函数内的语句有效(这个例子中就是输出运算符要有效),那么把lambda
传递给std::visit()
就可以正常编译。
预处理代码如下:
#include
#include
#include
using namespace std;
int main()
{
class __lambda_8_15
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(const type_parameter_0_0 & val) const
{
operator<<(operator<<(std::cout, val), '\n');
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<int>(const int & val) const
{
std::operator<<(std::cout.operator<<(val), '\n');
}
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(const type_parameter_0_0 & val)
{
return __lambda_8_15{}.operator()<type_parameter_0_0>(val);
}
};
__lambda_8_15 pp = __lambda_8_15{};
std::variant<int> var = std::variant<int>(42);
std::visit(pp, var);
return 0;
}
你也可以使用lambda
来修改当前选项的值:
#include
#include
#include
using namespace std;
int main() {
auto pp = [](auto& val) {
std::cout << val << '\n'
val = val + val;
};
std::variant<int> var(42);
std::visit(pp, var);//输出42
cout << std::get<0>(var) << endl; // 输出84
// 将当前选项的值变为两倍:
std::visit([](auto& val) { val = val + val; }, var);
cout << std::get<0>(var) << endl;//输出168
}
或者:
#include
#include
#include
using namespace std;
int main() {
std::variant<int> var(42);
// 将当前选项的值设为默认值
std::visit(
[](auto& val) { val = std::remove_reference_t<decltype(val)>{}; }, var);
cout << std::get<0>(var) << endl; //输出0
}
你甚至可以使用编译期if
语句来对不同的选项类型进行不同的处理。例如:
#include
#include
#include
using namespace std;
int main() {
auto dblvar = [](auto& val) {
if constexpr (std::is_convertible_v<decltype(val), std::string>) {
val = val + val;
} else {
val *= 2;
}
};
std::variant<int> var(42);
std::visit(dblvar, var);
cout << std::get<0>(var) << endl;
}
预处理代码如下:
using namespace std;
int main()
{
class __lambda_8_19
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 & val) const
{
if constexpr(std::is_convertible_v<decltype(val), std::basic_string<char> >) {
val = operator+(val, val);
} else /* constexpr */ {
val = static_cast<type_parameter_0_0>(static_cast<<dependent type>>(val) * 2);
}
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<int>(int & val) const
{
if constexpr(false) {
} else /* constexpr */ {
val = static_cast<int>(val * 2);
}
}
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 & val)
{
return __lambda_8_19{}.operator()<type_parameter_0_0>(val);
}
};
__lambda_8_19 dblvar = __lambda_8_19{};
std::variant<int> var = std::variant<int>(42);
std::visit(dblvar, var);
std::cout.operator<<(std::get<0>(var)).operator<<(std::endl);
return 0;
}
这里,对于std::string
类型的选项,泛型lambda
会把函数调用模板实例化为计算:
val = val + val;
而对于其他类型的选项,例如int
或double
,lambda函数调用模板会实例化为计算:
val *= 2;
注意检查val
的类型时必须小心。这里,我们检查了val
的类型是否能转换为std::string
。如下检查:
if constexpr(std::is_same_v<decltype(val), std::string>) {
将不能正确工作,因为val
的类型只可能是int&、std::string&、long double&
这样的引用类型。
访问器中的函数调用可以返回值,但所有返回值类型必须相同。例如:
#include
#include
#include
#include
using namespace std;
int main() {
using IntOrDouble = std::variant<int, double>;
std::vector<IntOrDouble> coll{42, 7.7, 0, -0.7};
double sum{0};
for (const auto& elem : coll) {
sum += std::visit(
[](const auto& val) -> double {
cout << val << endl;
return val;
},
elem);
}
cout << sum << endl;
}
输出结果如下:
42
7.7
0
-0.7
49
上面的代码会把所有选项的值加到sum
上。如果lambda
中没有显式指明返回类型将不能通过编译,因为自动推导的话返回类型会不同。
lambda
作为访问器通过使用函数对象和lambda
的 重载器(overloader) ,可以定义一系列lambda
,其中最佳的匹配将会被用作访问器。
假设有一个如下定义的overload
重载器:
// 继承所有基类里的函数调用运算符
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
// 基类的类型从传入的参数中推导
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;
你可以为每个可能的选项提供一个lambda
,之后使用overload
来访问variant
:
std::variant<int, std::string> var(42);
...
std::visit(overload { // 为当前选项调用最佳匹配的lambda
[](int i) { std::cout << "int: " << i << '\n'; },
[] (const std::string& s) {
std::cout << "string: " << s << '\n';
},
}, var);
你也可以使用泛型lambda
,它可以匹配所有情况。例如,要想修改一个variant
当前选项的值,你可以使用重载实现字符串和其他类型值“翻倍”:
auto twice = overload {
[] (std::string& s) { s += s; },
[] (auto& i) { i *= 2; },
};
使用这个重载,对于字符串类型的选项,值将变为原本的字符串再重复一遍;而对于其他类型,将会把值乘以2。下面展示了怎么应用于variant
:
std::variant<int, std::string> var(42);
std::visit(twice, var); // 值42变为84
...
var = "hi";
std::visit(twice, var); // 值"hi"变为"hihi"
示例:
// visitVariantsOverloadPattern.cpp
#include
#include
#include
#include
#include
template<typename ... Ts> // (7)
struct Overload : Ts ... {
using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>;
int main(){
std::cout << '\n';
std::vector<std::variant<char, long, float, int, double, long long>> // (1)
vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017};
auto TypeOfIntegral = Overload { // (2)
[](char) { return "char"; },
[](int) { return "int"; },
[](unsigned int) { return "unsigned int"; },
[](long int) { return "long int"; },
[](long long int) { return "long long int"; },
[](auto) { return "unknown type"; },
};
for (auto v : vecVariant) { // (3)
std::cout << std::visit(TypeOfIntegral, v) << '\n';
}
std::cout << '\n';
std::vector<std::variant<std::vector<int>, double, std::string>> // (4)
vecVariant2 = { 1.5, std::vector<int>{1, 2, 3, 4, 5}, "Hello "};
auto DisplayMe = Overload { // (5)
[](std::vector<int>& myVec) {
for (auto v: myVec) std::cout << v << " ";
std::cout << '\n';
},
[](auto& arg) { std::cout << arg << '\n';},
};
for (auto v : vecVariant2) { // (6)
std::visit(DisplayMe, v);
}
std::cout << '\n';
}
运行结果
int
char
unknown type
long long int
long int
unknown type
int
1.5
1 2 3 4 5
Hello
预处理代码如下:
// visitVariantsOverloadPattern.cpp
#include
#include
#include
#include
#include
template<typename ... Ts>
struct Overload : public Ts...
{
using Ts::operator()...;
};
/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> : public __lambda_23_9, public __lambda_24_9, public __lambda_25_9, public __lambda_26_9, public __lambda_27_9, public __lambda_28_9
{
using __lambda_23_9::operator();
// inline /*constexpr */ const char * ::operator()(char) const;
using __lambda_24_9::operator();
// inline /*constexpr */ const char * ::operator()(int) const;
using __lambda_25_9::operator();
// inline /*constexpr */ const char * ::operator()(unsigned int) const;
using __lambda_26_9::operator();
// inline /*constexpr */ const char * ::operator()(long) const;
using __lambda_27_9::operator();
// inline /*constexpr */ const char * ::operator()(long long) const;
using __lambda_28_9::operator();
template<class type_parameter_0_0>
inline /*constexpr */ auto ::operator()(type_parameter_0_0) const
{
return "unknown type";
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto ::operator()<char>(char) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto ::operator()<long>(long) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ const char * ::operator()<float>(float) const
{
return "unknown type";
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto ::operator()<int>(int) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ const char * ::operator()<double>(double) const
{
return "unknown type";
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto ::operator()<long long>(long long) const;
#endif
// inline constexpr Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> & operator=(const Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> &) /* noexcept */ = delete;
// inline constexpr Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> & operator=(Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> &&) /* noexcept */ = delete;
};
#endif
/* First instantiated from: insights.cpp:40 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Overload<__lambda_41_9, __lambda_45_9> : public __lambda_41_9, public __lambda_45_9
{
using __lambda_41_9::operator();
// inline /*constexpr */ void ::operator()(std::vector > & myVec) const;
using __lambda_45_9::operator();
template<class type_parameter_0_0>
inline /*constexpr */ auto ::operator()(type_parameter_0_0 & arg) const
{
(std::cout << arg) << '\n';
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto ::operator()<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> > & arg) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void ::operator()<double>(double & arg) const
{
std::operator<<(std::cout.operator<<(arg), '\n');
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void ::operator()<std::basic_string<char> >(std::basic_string<char> & arg) const
{
std::operator<<(std::operator<<(std::cout, arg), '\n');
}
#endif
// inline constexpr Overload<__lambda_41_9, __lambda_45_9> & operator=(const Overload<__lambda_41_9, __lambda_45_9> &) /* noexcept */ = delete;
// inline constexpr Overload<__lambda_41_9, __lambda_45_9> & operator=(Overload<__lambda_41_9, __lambda_45_9> &&) /* noexcept */ = delete;
};
#endif
template<class... Ts> Overload(Ts...) -> Overload<Ts...>;
/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Overload(__lambda_23_9 __0, __lambda_24_9 __1, __lambda_25_9 __2, __lambda_26_9 __3, __lambda_27_9 __4, __lambda_28_9 __5) -> Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9>;
#endif
/* First instantiated from: insights.cpp:40 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Overload(__lambda_41_9 __0, __lambda_45_9 __1) -> Overload<__lambda_41_9, __lambda_45_9>;
#endif
int main()
{
std::operator<<(std::cout, '\n');
std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > vecVariant = std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > >{std::initializer_list<std::variant<char, long, float, int, double, long long> >{std::variant<char, long, float, int, double, long long>(5), std::variant<char, long, float, int, double, long long>('2'), std::variant<char, long, float, int, double, long long>(5.4000000000000004), std::variant<char, long, float, int, double, long long>(100LL), std::variant<char, long, float, int, double, long long>(2011L), std::variant<char, long, float, int, double, long long>(3.5F), std::variant<char, long, float, int, double, long long>(2017)}, std::allocator<std::variant<char, long, float, int, double, long long> >()};
class __lambda_23_9
{
public:
inline /*constexpr */ const char * operator()(char) const
{
return "char";
}
using retType_23_9 = const char *(*)(char);
inline constexpr operator retType_23_9 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ const char * __invoke(char __param0)
{
return __lambda_23_9{}.operator()(__param0);
}
public:
// inline /*constexpr */ __lambda_23_9 & operator=(const __lambda_23_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_23_9(__lambda_23_9 &&) noexcept = default;
};
class __lambda_24_9
{
public:
inline /*constexpr */ const char * operator()(int) const
{
return "int";
}
using retType_24_9 = const char *(*)(int);
inline constexpr operator retType_24_9 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ const char * __invoke(int __param0)
{
return __lambda_24_9{}.operator()(__param0);
}
public:
// inline /*constexpr */ __lambda_24_9 & operator=(const __lambda_24_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_24_9(__lambda_24_9 &&) noexcept = default;
};
class __lambda_25_9
{
public:
inline /*constexpr */ const char * operator()(unsigned int) const
{
return "unsigned int";
}
using retType_25_9 = const char *(*)(unsigned int);
inline constexpr operator retType_25_9 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ const char * __invoke(unsigned int __param0)
{
return __lambda_25_9{}.operator()(__param0);
}
public:
// inline /*constexpr */ __lambda_25_9 & operator=(const __lambda_25_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_25_9(__lambda_25_9 &&) noexcept = default;
};
class __lambda_26_9
{
public:
inline /*constexpr */ const char * operator()(long) const
{
return "long int";
}
using retType_26_9 = const char *(*)(long);
inline constexpr operator retType_26_9 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ const char * __invoke(long __param0)
{
return __lambda_26_9{}.operator()(__param0);
}
public:
// inline /*constexpr */ __lambda_26_9 & operator=(const __lambda_26_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_26_9(__lambda_26_9 &&) noexcept = default;
};
class __lambda_27_9
{
public:
inline /*constexpr */ const char * operator()(long long) const
{
return "long long int";
}
using retType_27_9 = const char *(*)(long long);
inline constexpr operator retType_27_9 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ const char * __invoke(long long __param0)
{
return __lambda_27_9{}.operator()(__param0);
}
public:
// inline /*constexpr */ __lambda_27_9 & operator=(const __lambda_27_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_27_9(__lambda_27_9 &&) noexcept = default;
};
class __lambda_28_9
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0) const
{
return "unknown type";
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto operator()<char>(char) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto operator()<long>(long) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ const char * operator()<float>(float) const
{
return "unknown type";
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto operator()<int>(int) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ const char * operator()<double>(double) const
{
return "unknown type";
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto operator()<long long>(long long) const;
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 __param0)
{
return __lambda_28_9{}.operator()<type_parameter_0_0>(__param0);
}
public:
// inline /*constexpr */ __lambda_28_9 & operator=(const __lambda_28_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_28_9(__lambda_28_9 &&) noexcept = default;
};
Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> TypeOfIntegral = Overload{__lambda_23_9(__lambda_23_9{}), __lambda_24_9(__lambda_24_9{}), __lambda_25_9(__lambda_25_9{}), __lambda_26_9(__lambda_26_9{}), __lambda_27_9(__lambda_27_9{}), __lambda_28_9(__lambda_28_9{})};
{
std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > & __range1 = vecVariant;
__gnu_cxx::__normal_iterator<std::variant<char, long, float, int, double, long long> *, std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > > __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<std::variant<char, long, float, int, double, long long> *, std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > > __end1 = __range1.end();
for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
std::variant<char, long, float, int, double, long long> v = std::variant<char, long, float, int, double, long long>(__begin1.operator*());
std::operator<<(std::operator<<(std::cout, std::visit(TypeOfIntegral, v)), '\n');
}
}
std::operator<<(std::cout, '\n');
std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > vecVariant2 = std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > >{std::initializer_list<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > >{std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >(1.5), std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >(std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3, 4, 5}, std::allocator<int>()}), std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >("Hello ")}, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > >()};
class __lambda_41_9
{
public:
inline /*constexpr */ void operator()(std::vector<int, std::allocator<int> > & myVec) const
{
{
std::vector<int, std::allocator<int> > & __range1 = myVec;
__gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __end1 = __range1.end();
for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
int v = __begin1.operator*();
std::operator<<(std::cout.operator<<(v), " ");
}
}
std::operator<<(std::cout, '\n');
}
using retType_41_9 = void (*)(std::vector<int> &);
inline constexpr operator retType_41_9 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ void __invoke(std::vector<int, std::allocator<int> > & myVec)
{
__lambda_41_9{}.operator()(myVec);
}
public:
// inline /*constexpr */ __lambda_41_9 & operator=(const __lambda_41_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_41_9(__lambda_41_9 &&) noexcept = default;
};
class __lambda_45_9
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 & arg) const
{
(std::cout << arg) << '\n';
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ auto operator()<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> > & arg) const;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<double>(double & arg) const
{
std::operator<<(std::cout.operator<<(arg), '\n');
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<std::basic_string<char> >(std::basic_string<char> & arg) const
{
std::operator<<(std::operator<<(std::cout, arg), '\n');
}
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 & arg)
{
return __lambda_45_9{}.operator()<type_parameter_0_0>(arg);
}
public:
// inline /*constexpr */ __lambda_45_9 & operator=(const __lambda_45_9 &) /* noexcept */ = delete;
// inline /*constexpr */ __lambda_45_9(__lambda_45_9 &&) noexcept = default;
};
Overload<__lambda_41_9, __lambda_45_9> DisplayMe = Overload{__lambda_41_9(__lambda_41_9{}), __lambda_45_9(__lambda_45_9{})};
{
std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > & __range1 = vecVariant2;
__gnu_cxx::__normal_iterator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > *, std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > > __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > *, std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > > __end1 = __range1.end();
for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > v = std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >(__begin1.operator*());
std::visit(DisplayMe, v);
}
}
std::operator<<(std::cout, '\n');
return 0;
}
如果你赋给一个variant
新值时发生了异常,那么这个variant
可能会进入一个非常特殊的状态:它已经失去了旧的值但还没有获得新的值。例如:
struct S {
operator int() { throw "EXCEPTION"; } // 转换为int时会抛出异常
};
std::variant<double, int> var{12.2}; // 初始化为double
var.emplace<1>(S{}); // OOPS:当设为int时抛出异常
如果这种情况发生了,那么:
var.valueless_by_exception()
会返回true
var.index()
会返回std::variant_npos
这些都标志该variant
当前没有值。
这种情况下有如下保证:
emplace()
抛出异常,那么valueless_by_exception()
可能会返回true
。operator=()
抛出异常且该修改不会改变选项,那么index()
和valueless_by_exception()
的状态将保持不变。值的状态依赖于值类型的异常保证。operator=()
抛出异常且新值是新的选项,那么variant 可能 会没有值(valueless_by_exception()
可能 会返回true
)。具体情况依赖于异常抛出的时机。如果发生在实际修改值之前的类型转换期间,那么variant
将依然持有旧值。variant
,你需要检查它的状态。例如:std::variant<double, int> var{12.2}; // 初始化为double
try {
var.emplace<1>(S{}); // OOPS:设置为int时抛出异常
}
catch (...) {
if (!var.valueless_by_exception()) {
...
}
}
std::variant
实现多态的异质集合std::variant
允许一种新式的多态性,可以用来实现异质集合。这是一种带有闭类型集合的运行时多态性。
关键在于variant<>
可以持有多种选项类型的值。可以将元素类型定义为variant
来实现异质的集合,这样的集合可以持有不同类型的值。因为每一个variant
知道当前的选项,并且有了访问器接口,我们可以定义在运行时根据不同类型进行不同操作的函数/方法。同时因为variant
有值语义,所以我们不需要指针(和相应的内存管理)或者虚函数。
std::variant
实现几何对象例如,假设我们要负责编写表示几何对象的库:
#include
#include
#include
#include "coord.hpp"
#include "line.hpp"
#include "circle.hpp"
#include "rectangle.hpp"
// 所有几何类型的公共类型
using GeoObj = std::variant<Line, Circle, Rectangle>;
// 创建并初始化一个几何体对象的集合
std::vector<GeoObj> createFigure()
{
std::vector<GeoObj> f;
f.push_back(Line{Coord{1, 2}, Coord{3, 4}});
f.push_back(Circle{Coord{5, 5}, 2});
f.push_back(Rectangle{Coord{3, 3}, Coord{6, 4}});
return f;
}
int main()
{
std::vector<GeoObj> figure = createFigure();
for (const GeoObj& geoobj : figure) {
std::visit([] (const auto& obj) {
obj.draw(); // 多态性调用draw()
}, geoobj);
}
}
首先,我们为所有可能的类型定义了一个公共类型:
using GeoObj = std::variant<Line, Circle, Rectangle>;
这三个类型不需要有任何特殊的关系。事实上它们甚至没有一个公共的基类、没有任何虚函数、接口也可能不同。例如:
#ifndef CIRCLE_HPP
#define CIRCLE_HPP
#include "coord.hpp"
#include
class Circle {
private:
Coord center;
int rad;
public:
Circle (Coord c, int r) : center{c}, rad{r} {
}
void move(const Coord& c) {
center += c;
}
void draw() const {
std::cout << "circle at " << center << " with radius " << rad << '\n';
}
};
#endif
我们现在可以创建相应的对象并把它们以值传递给容器,最后可以得到这些类型的元素的集合:
std::vector<GeoObj> createFigure()
{
std::vector<GeoObj> f;
f.push_back(Line{Coord{1, 2}, Coord{3, 4}});
f.push_back(Circle{Coord{5, 5}, 2});
f.push_back(Rectangle{Coord{3, 3}, Coord{6, 4}});
return f;
}
以前如果没有使用继承和多态的话是不可能写出这样的代码的。以前要想实现这样的异构集合,所有的类型都必须继承自GeoObj
,并且最后将得到一个元素类型为GeoObj
的指针的vector
。为了使用指针,必须用new
创建新对象,这导致最后还要追踪什么时候调用delete
,或者要使用智能指针来完成(unique_ptr
或者shared_ptr
)。
现在,通过使用访问器,我们可以迭代每一个元素,并依据元素的类型“做正确的事情”:
std::vector<GeoObj> figure = createFigure();
for (const GeoObj& geoobj : figure) {
std::visit([] (const auto& obj) {
obj.draw(); // 多态调用draw()
}, geoobj);
}
这里,visit()
使用了泛型lambda
来为每一个可能的GeoObj
类型实例化。也就是说,当编译visit()
调用时,lambda
将会被实例化并编译为3个函数:
Line
编译代码:[] (const Line& obj) {
obj.draw(); // 调用Line::draw()
}
Circle
编译代码:[] (const Circle& obj) {
obj.draw(); // 调用Circle::draw()
}
Rectangle
编译代码:[] (const Rectangle& obj) {
obj.draw(); // 调用Rectangle::draw()
}
如果这些实例中有一个不能编译,那么对visit()
的调用也不能编译。如果所有实例都能编译,那么将保证会对所有元素类型调用相应的函数。注意生成的代码并不是 if-else 链。C++
标准保证这些调用的性能不会依赖于variant
选项的数量。
也就是说,从效率上讲,这种方式和虚函数表的方式的行为相同(通过类似于为所有visit()
创建局部虚函数表的方式)。注意,draw()
函数不需要是虚函数。
如果对不同类型的操作不同,我们可以使用编译期if
语句或者重载访问器来处理不同的情况
std::variant
实现其他异质集合考虑如下另一个使用std::variant<>
实现异质集合的例子:
#include
#include
#include
#include
#include
int main()
{
using Var = std::variant<int, double, std::string>;
std::vector<Var> values {42, 0.19, "hello world", 0.815};
for (const Var& val : values) {
std::visit([] (const auto& v) {
if constexpr(std::is_same_v<decltype(v), const std::string&>) {
std::cout << '"' << v << "\" ";
}
else {
std::cout << v << ' ';
}
}, val);
}
}
我们又一次定义了自己的类型来表示若干可能类型中的一个:
using Var = std::variant<int, double, std::string>;
我们可以用它创建并初始化一个异质的集合:
std::vector<Var> values {42, 0.19, "hello world", 0.815};
注意我们可以用若干异质的元素来实例化vector
,因为它们都能自动转换为variant
类型。然而,如果我们还传递了一个long
类型的初值,上面的初始化将不能编译,因为编译器不能决定将它转换为int
还是double
。
当我们迭代元素时,我们使用了访问器来调用相应的函数。这里使用了一个泛型lambda。lambda为3种可能的类型分别实例化了一个函数调用。为了对字符串进行特殊的处理(在输出值时用双引号包括起来),我们使用了编译期if
语句:
for (const Var& val : values) {
std::visit([] (const auto& v) {
if constexpr(std::is_same_v<decltype(v), const std::string&>) {
std::cout << '"' << v << "\" ";
}
else {
std::cout << v << ' ';
}
}, val);
}
这意味着输出将是:
42 0.19 "hello world" 0.815
通过使用重载的访问器,我们可以像下面这样实现:
for (const auto& val : values) {
std::visit(overload {
[] (const auto& v) {
std::cout << v << ' ';
},
[] (const std::string& v) {
std::cout << '"' << v "\" ";
}
}, val);
}
然而,注意这样可能会陷入重载匹配的问题。有的情况下泛型lambda(即函数模板)匹配度比隐式类型更高,这意味着可能会调用错误的类型。
variant
让我们来总结一下使用std::variant
实现多态的异构集合的优点和缺点:
优点:
virtual
成员函数,不需要虚函数表,因此开销更小unique
或者 智能指针(不会出现访问已释放内存或内存泄露等问题)vector
中的元素是连续存放在一起的(原本指针的方式所有元素是散乱分布在堆内存中的)缺点:
一般来说,我并不确定是否要推荐默认使用std::variant<>
来实现多态。一方面这种方法很安全(没有指针,意味着没有new
和delete
),也不需要虚函数。然而另一方面,使用访问器有一些笨拙,有时你可能会需要引用语义(在多个地方使用同一个对象),还有在某些情形下并不能在编译期确定所有的类型。
性能开销也有很大不同。没有了new
和delete
可能会减少很大开销。但另一方面,以值传递对象又可能会增大很多开销。在实践中,你必须自己测试对你的代码来说哪种方法效率更高。
特定类型的variant
可能导致特殊或者出乎意料的行为。
bool
和std::string
选项如果一个std::variant<>
同时有bool
和std::string
选项,赋予一个字符串字面量可能会导致令人惊奇的事,因为字符串字面量会优先转换为bool
,而不是std::string
。例如:
std::variant<bool, std::string> v;
v = "hi"; // OOPS:设置bool选项
std::cout << "index: " << v.index() << '\n';
std::visit([] (const auto& val) {
std::cout << "value: " << val << '\n';
}, v);
这段代码片段将会有如下输出:
index: 0
value: true
可以看出,字符串字面量会被解释为把variant的bool选项初始化为true
(因为指针不是0
所以是true
)。
这里有一些修正这个赋值问题的方法:
v.emplace<1>("hello"); // 显式赋值给第二个选项
v.emplace<std::string>("hello"); // 显式赋值给string选项
v = std::string{"hello"}; // 确保用string赋值
using namespace std::literals; // 确保用string赋值
v = "hello"s;
[1] Replacing Unique_ptr With C++17’s std::variant — a Practical Experiment
[2] Visiting a std::variant with the Overload Pattern
[3] std::variant
[4] Modern C++ Features – std::variant and std::visit
[5] Everything You Need to Know About std::variant from C++17