std::variant
是 C++17 中引入的标准库类型,用于表示多个可能类型中的一个。它类似于联合体(union),但更安全且易于使用。 std::variant
提供了一种类型安全的方式来处理不同类型的值。
如果你用过 C 语言,你肯定用过 union。在 C 编程语言中,union 是一种复合数据类型,允许在同一内存位置存储不同类型的数据。与结构体一样,union 可以有多个成员,但与结构体不同的是,union 分配的内存只够容纳其最大的成员。这就意味着,union 的所有成员共享同一个内存空间。
std::variant
适用于之前使用 union
的场景。
让我们看看下面的 C 代码:
Copy #include <stdint.h>
#include <stdio.h>
union
{
struct
{
uint8_t low : 4 ; // 表示该位域占用4个位,这使得我们能够直接操作和访问字节的高低半部分
uint8_t high : 4 ;
}nibles;
uint8_t bytes;
}myByte;
int main ()
{
myByte . bytes = 0x AB ;
printf( "High Nible : 0x %X \n" , myByte . nibles . high) ;
printf( "Low Nible : 0x %X \n" , myByte . nibles . low) ;
return 0 ;
}
输出结果:
Copy High Nible : 0x A
Low Nible : 0x B
1010
,对应十六进制的 A
1011
,对应十六进制的 B
如上所述,它分配的内存与联合体中最长的元素一样多。所以是 1 个字节。它将写入其中的变量放在相同的地址。这样,如果名为 bytes 的变量发生变化,那么 union 中的另一个元素,即名为 nibles 的结构也会发生变化。
但是联合体在提供便利的同时也带来了一些麻烦,看下面的代码:
Copy #include <iostream>
union MyUnion {
int intValue;
double doubleValue;
};
int main () {
MyUnion myUnion;
myUnion . doubleValue = 3.14 ;
std::cout << "Integer Value: " << myUnion . intValue << "\n" ;
return 0 ;
}
我们将 3.14 赋值给 double 变量。然后我们尝试访问 int 变量。结果如下:
Copy Integer Value: 1374389535
结果有些莫名其妙,事实上我们不应该访问int类型的那个变量,在 C++17 中出现的 std::variant,就是为了解决这个潜在的问题,他为我们提供了类型安全的联合体。
什么是std::variant
std::variant 是 C++17 的一个特性,它提供了一个类型安全的联合体。它是 C++ 标准库的一部分,定义在 头文件中。
在 C++ 中,union 允许你在同一内存位置存储不同类型的数据,但它不提供类型安全。std::variant 是对传统 union 的改进,因为它确保了类型安全,并为处理不同类型的值提供了一种方便的方法。
让我们先测试一下类型安全功能。
Copy #include <iostream>
#include <variant>
int main () {
std::variant <int , double> myVariant;
myVariant = 42 ; /* Store an int */
std::cout << std::get <double> (myVariant) << std::endl;
return 0 ;
}
// g++ -std=c++17 demo.cpp -o demo
输出结果为:
Copy terminate called after throwing an instance of 'std::bad_variant_access'
what (): Unexpected index
Aborted (core dumped)
我们创建了一个包含 int 和 double 的 std::variant 变量。我们将值 42 赋给 int 变量。然后,我们尝试访问 double 变量,得到了一个错误。就是这样!这就是类型安全!
我们再来看一个使用 std::variant 的例子。
Copy #include <iostream>
#include <variant>
#include <string>
int main () {
/* Define std::variant; it can hold either an int or a std::string value */
std::variant <int , std::string > myVariant;
/* Assign an int to std::variant */
myVariant = 42 ;
/* Assign a std::string to std::variant */
myVariant = "Hello, Variant!" ;
/* Retrieve and use the value from std::variant */
try {
/* It's important to check which type is stored in std::variant before retrieving the value */
if (std::holds_alternative <int> (myVariant))
{
std::cout << "Value as int: " << std::get <int> (myVariant) << std::endl;
}
else if (std::holds_alternative < std::string > (myVariant))
{
std::cout << "Value as string: " << std::get < std::string > (myVariant) << std::endl;
}
else
{
std::cout << "Unknown type!" << std::endl;
}
} catch (const std::bad_variant_access& e)
{
std::cerr << "Error: " << e . what () << std::endl;
}
return 0 ;
}
输出结果是:
Copy Value as string: Hello , Variant !
在主函数中,定义了一个名为 myVariant 的 std::variant,它可以保存一个 int 或一个 std::string。一个 int 值(本例中为 42)被赋值给 std::variant。然后一个 std::string ("Hello, Variant!")
被分配给同一个 std::variant。这表明 std::variant 可以灵活地保存不同类型的值。try-catch 块用于处理访问存储在 std::variant 中的值时可能出现的异常。它使用 std::holds_alternative 检查存储值的类型,然后提取并打印相应的值。如果遇到未知类型,则会打印错误信息。如果出现异常(例如试图访问错误的类型),它会捕获异常并使用 std::cerr 打印错误信息。
std::variant
公共成员函数
默认构造函数:
Copy std::variant <int , double , std::string > myVariant; /* Default construction */
赋值构造函数
Copy std::variant <int , double> myVariant;
myVariant = 42 ; /* Assigns an int */
使用 index
方法获取当前持有类型的索引
index
方法返回 std::variant
当前持有类型在模板参数列表中的索引。
Copy #include <variant>
#include <iostream>
#include <string>
int main () {
std :: variant <int , float , std :: string > var;
var = 42 ;
std :: cout << "Current index: " << var . index () << std :: endl; // 输出 0
var = 3.14 f ;
std :: cout << "Current index: " << var . index () << std :: endl; // 输出 1
var = "Hello" ;
std :: cout << "Current index: " << var . index () << std :: endl; // 输出 2
return 0 ;
}
在这个示例中,index
方法返回了 std::variant
持有值的索引。索引 0
表示 int
类型,索引 1
表示 float
类型,索引 2
表示 std::string
类型。
使用 std::variant_alternative
获取类型
std::variant_alternative
可以用来获取 std::variant
模板参数列表中指定索引处的类型。
Copy #include <variant>
#include <iostream>
#include <string>
#include <type_traits>
int main () {
std :: variant <int , float , std :: string > var;
// 获取索引 0 对应的类型
using T0 = std :: variant_alternative< 0 , decltype (var)> :: type ;
std :: cout << "Type at index 0: " << typeid (T0) . name () << std :: endl; // 输出 int
// 获取索引 1 对应的类型
using T1 = std :: variant_alternative< 1 , decltype (var)> :: type ;
std :: cout << "Type at index 1: " << typeid (T1) . name () << std :: endl; // 输出 float
// 获取索引 2 对应的类型
using T2 = std :: variant_alternative< 2 , decltype (var)> :: type ;
std :: cout << "Type at index 2: " << typeid (T2) . name () << std :: endl; // 输出 std::string
return 0 ;
}
在这个示例中,std::variant_alternative
用来获取 std::variant
模板参数列表中指定索引处的类型。std::variant_alternative<0, decltype(var)>::type
获取了 var
的第一个类型 int
,std::variant_alternative<1, decltype(var)>::type
获取了第二个类型 float
,依此类推。
综合示例
Copy #include <variant>
#include <iostream>
#include <string>
#include <typeinfo>
int main () {
std :: variant <int , float , std :: string > var = "Hello" ;
std :: cout << "Current index: " << var . index () << std :: endl; // 输出 2
if ( var . index () == 0 ) {
using T = std :: variant_alternative< 0 , decltype (var)> :: type ;
std :: cout << "Current type: " << typeid (T) . name () << " with value " << std :: get < T >(var) << std :: endl;
} else if ( var . index () == 1 ) {
using T = std :: variant_alternative< 1 , decltype (var)> :: type ;
std :: cout << "Current type: " << typeid (T) . name () << " with value " << std :: get < T >(var) << std :: endl;
} else if ( var . index () == 2 ) {
using T = std :: variant_alternative< 2 , decltype (var)> :: type ;
std :: cout << "Current type: " << typeid (T) . name () << " with value " << std :: get < T >(var) << std :: endl;
}
return 0 ;
}
Valueless by Exception
valueless_by_exception()
用于查看variant变量是否赋值了。
Copy std :: variant <int , double> myVariant;
if ( myVariant . valueless_by_exception ()) {
/* Handle the case where the variant has no value */
}
Swap
用于交换两个std::variant
的数值(注意:需要类型一致)
Copy std :: variant <int , double> var1 = 42 ;
std :: variant <int , double> var2 = 3.14 ;
std :: swap (var1 , var2);
empalce()
emplace() 使用提供的参数进行就地构造 。
Copy std::variant <int , std::string > myVariant;
myVariant.emplace < std::string > ( "Hello" );
visit()
std::visit
函数用于访问 std::variant
中存储的值。它通过接受一个访问者(visitor)对象来对 std::variant
中当前存储的值进行操作。访问者对象可以是一个函数对象、lambda 表达式或具有重载 operator()
的结构体。
std::visit
的基本用法包括:
定义一个 std::variant
对象,包含多个可能的类型。
使用 std::visit
传递访问者对象和 std::variant
对象。
详细示例
以下是详细的示例,展示如何使用 std::visit
来访问和操作 std::variant
中存储的值。
示例 1:基本用法
Copy #include <variant>
#include <iostream>
#include <string>
int main () {
std :: variant <int , float , std :: string > var = "Hello, World!" ;
auto visitor = []( auto&& arg) {
std :: cout << "Value: " << arg << std :: endl;
};
std :: visit (visitor , var); // 输出: Value: Hello, World!
var = 42 ;
std :: visit (visitor , var); // 输出: Value: 42
var = 3.14 f ;
std :: visit (visitor , var); // 输出: Value: 3.14
return 0 ;
}
示例 2:使用结构体作为访问者
Copy #include <variant>
#include <iostream>
#include <string>
struct Visitor {
void operator ()( int i) const {
std :: cout << "Integer: " << i << std :: endl;
}
void operator ()( float f) const {
std :: cout << "Float: " << f << std :: endl;
}
void operator ()( const std :: string & s) const {
std :: cout << "String: " << s << std :: endl;
}
};
int main () {
std :: variant <int , float , std :: string > var;
var = 42 ;
std :: visit (Visitor{} , var); // 输出: Integer: 42
var = 3.14 f ;
std :: visit (Visitor{} , var); // 输出: Float: 3.14
var = "Hello, World!" ;
std :: visit (Visitor{} , var); // 输出: String: Hello, World!
return 0 ;
}
示例 3:访问多个 std::variant
std::visit
还可以用于同时访问多个 std::variant
对象。此时需要一个接受多个参数的访问者对象。
Copy #include <variant>
#include <iostream>
#include <string>
struct Visitor {
void operator ()( int i , float f) const {
std :: cout << "Integer: " << i << ", Float: " << f << std :: endl;
}
void operator ()( int i , const std :: string & s) const {
std :: cout << "Integer: " << i << ", String: " << s << std :: endl;
}
void operator ()( float f , const std :: string & s) const {
std :: cout << "Float: " << f << ", String: " << s << std :: endl;
}
void operator ()( const auto& a , const auto& b) const {
std :: cout << "Other types: " << a << ", " << b << std :: endl;
}
};
int main () {
std :: variant <int , float , std :: string > var1 = 42 ;
std :: variant <int , float , std :: string > var2 = "Hello, World!" ;
std :: visit (Visitor{} , var1 , var2); // 输出: Integer: 42, String: Hello, World!
var2 = 3.14 f ;
std :: visit (Visitor{} , var1 , var2); // 输出: Integer: 42, Float: 3.14
return 0 ;
}
std::variant
的优势
如前所述,当访问错误的类型时,传统的联合体类型可能会导致意想不到的错误。
Copy std::variant <int , double , std::string > myVariant = 42 ;
// Type-safe access
std::cout << std::get <int> (myVariant) << std::endl;
明确表达了该变量有多种数据类型。
Copy std::variant <int , double , std::string > myVariant = "Hello" ;
// Improved readability
if (std::holds_alternative < std::string > (myVariant)) {
std::cout << std::get < std::string > (myVariant) << std::endl;
}
Copy #include <iostream>
#include <variant>
#include <string>
std::variant <int , double , std::string > getValue ( bool flag) {
if (flag) {
return 42 ; // 返回int类型
} else {
return "Hello" ; // 返回std::string类型
}
}
int main () {
std::variant <int , double , std::string > value = getValue( true ) ; // 调用函数并获取返回值
if (std::holds_alternative <int> (value)) {
std::cout << "Returned int: " << std::get <int> (value) << std::endl;
} else if (std::holds_alternative < std::string > (value)) {
std::cout << "Returned string: " << std::get < std::string > (value) << std::endl;
}
value = getValue( false ) ; // 再次调用函数,获取不同类型的返回值
if (std::holds_alternative <int> (value)) {
std::cout << "Returned int: " << std::get <int> (value) << std::endl;
} else if (std::holds_alternative < std::string > (value)) {
std::cout << "Returned string: " << std::get < std::string > (value) << std::endl;
}
return 0 ;
}
Copy #include <iostream>
#include <variant>
int main () {
auto example = [] ( int x) -> std::variant <int , std::string > {
if (x > 0 ) {
return x;
} else {
return "Negative or Zero" ;
}
};
auto result1 = example( 10 ) ;
auto result2 = example( - 5 ) ;
if (std::holds_alternative <int> (result1)) {
std::cout << "Result1: " << std::get <int> (result1) << std::endl;
} else {
std::cout << "Result1: " << std::get < std::string > (result1) << std::endl;
}
if (std::holds_alternative <int> (result2)) {
std::cout << "Result2: " << std::get <int> (result2) << std::endl;
} else {
std::cout << "Result2: " << std::get < std::string > (result2) << std::endl;
}
return 0 ;
Copy std::vector < std::variant <int , double , std::string >> data;
// Can use standard algorithms with std::variant
std:: for_each (data. begin () , data. end () , [] (auto & item) {
std::visit( [] (auto && arg){ std::cout << arg << std::endl; } , item) ;
});
总结
std::variant<T, U, ...>
代表一个多类型的容器,容器中的值是制定类型的一种,是通用的 Sum Type,对应 Rust 的enum
。是一种类型安全的union
,所以也叫做tagged union
。与union
相比有两点优势:
可以存储复杂类型,而 union 只能直接存储基础的 POD 类型,对于如std::vector
和std::string
就等复杂类型则需要用户手动管理内存。
类型安全,variant 存储了内部的类型信息,所以可以进行安全的类型转换,c++17 之前往往通过union
+enum
来实现相同功能。
通过使用std::variant<T, Err>
,用户可以实现类似 Rust 的std::result
,即在函数执行成功时返回结果,在失败时返回错误信息,上文的例子则可以改成:
Copy std::variant<ReturnType, Err> func(const string& in) {
ReturnType ret;
if (in.size() == 0)
return Err{"input is empty"};
// ...
return {ret};
}
需要注意的是,c++17 只提供了一个库级别的 variant 实现,没有对应的模式匹配(Pattern Matching)机制,而最接近的std::visit
又缺少编译器的优化支持,所以在 c++17 中std::variant
并不好用,跟 Rust 和函数式语言中出神入化的 Sum Type 还相去甚远,但是已经有许多围绕std::variant
的提案被提交给 c++委员会探讨,包括模式匹配,std::expected
等等。
reference