C++的新类型 optional, variant, any

std::optional

std::optional<T>代表一个可能存在的T值,实际上是一种Sum Type。常用于可能失败的函数的返回值中,比如工厂函数。在C++17之前,往往使用T*作为返回值,如果为nullptr则代表函数失败,否则T*指向了真正的返回值。但是这种写法模糊了所有权,函数的调用方无法确定是否应该接管T*的内存管理,而且T*可能为空的假设,如果忘记检查则会有SegFault的风险。

使用指针和optional的代码如下:

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
#include <optional>

// c++17 更安全和直观
std::optional<int> func(bool param)
{
if(param)
{
return 1;
}
return nullopt;
}

int* func2(bool param)
{
int p = 1;
int* ptr = &p;
if(param)
{
return ptr;
}
return nullptr;
}


auto f = func(false);
if(f.has_value() )
cout << f.value() << endl;

auto f2 = func2(false);
if(f2)
cout << *f2 << endl;

如果换成unique_ptr代替optional,后果是多一次堆内存分配。另外如果用了unique_ptr做类成员,这个类就不能被拷贝了,optional没有这个问题。二者的本质区别: unique_ptr是引用语义,optional是值语义

variant

std::variant 是 C++17 引入的一种类型安全的联合体,用来存储多个可能类型中的一种值,且保证使用时的类型安全。相比于传统的 union, std::variant不仅能够存储不同类型的值,还能自动管理复杂类型的构造与析构。

使用 std::variant 可以定义一个变量,该变量可以持有多种不同类型的值,但一次只能存储一种。std::variant 提供了一种类型安全的方式来处理多种类型,可以用在函数参数中以接受这些类型。

构造和赋值:您可以直接使用各种支持的类型初始化 std::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
std::variant<int, float, std::string> v;  // v 可以是 int、float 或 std::string
cout << "size of v: "<<sizeof (v) << endl;
v = 42;
cout << std::get<int>(v) << endl;

v = 12.3f;
cout << std::get<float>(v) << endl;
cout << std::get<1>(v) << endl;

// 可以使用 std::holds_alternative<T>(variant) 来判断 std::variant 当前是否持有某种类型
if(std::holds_alternative<int>(v) )
std::cout << "v holds an int" << std::endl;
else
std::cout << "v doesn't holds an int" << std::endl;

try {
std::cout << std::get<int>(v); // 当前不是 int 类型,会抛出异常
}
catch (const std::bad_variant_access& e)
{
std::cout << "Wrong type access: " << e.what() << std::endl;
}

//使用 v.index() 获取当前存储值的类型在 std::variant 中的索引
std::cout << "current value index: " << v.index() << std::endl; // 0 表示 int,1 表示 float,依次类推

v = "Hello, std::variant!";
cout << std::get<std::string>(v) << endl;

std::variant 可以作为函数参数,接受多种类型的值,这些类型是在定义 std::variant 时指定的

std::variant 主要用于以下场景:

  1. 多类型返回值: 一个函数可能返回不同类型的值,例如成功返回数据,失败返回错误信息
  2. 替代 union: std::variant 是 union 的现代、安全替代品,支持更多的类型和安全检查
  3. std::variant 可以表示有限状态机的状态,每个状态用不同的类型表示

类型安全: 使用 std::visit 可确保访问的值是有效的,避免了类型错误

1
2
3
std::visit([](auto&& arg) {
std::cout << "use visit to print value: " << arg << std::endl; // 打印不同类型的值
}, v);

std::any

std::any是一个可以存储任何可拷贝类型的容器,C语言中通常使用void*实现类似的功能,与void*相比,std::any具有两点优势:

std::any更安全:在类型T被转换成void*时,T的类型信息就已经丢失了,在转换回具体类型时程序无法判断当前的void*的类型是否真的是T,容易带来安全隐患。而std::any会存储类型信息,std::any_cast是一个安全的类型转换。
std::any管理了对象的生命周期,在std::any析构时,会将存储的对象析构,而void*则需要手动管理内存。
std::any应当很少是程序员的第一选择,在已知类型的情况下,std::optional, std::variant和继承都是比它更高效、更合理的选择。只有当对类型完全未知的情况下,才应当使用std::any,比如动态类型文本的解析或者业务逻辑的中间层信息传递。