1. std::variant (变体)

std::variant 是一个类型安全的联合体 (union)。它可以在编译时定义一个固定且有限的类型集合,并在运行时精确地持有其中一个类型的值。你总是知道 std::variant 可能包含哪些类型。

核心思想: “这个变量要么是一个 int要么是一个 double要么是一个 std::string,但它在任何时候都只可能是其中一个。”

特点:

  • 编译时已知类型集: 在声明 std::variant 时,你需要明确列出它可能包含的所有类型。
  • 类型安全: 你不能意外地从 std::variant 中提取一个它当前不持有的类型的值。如果尝试这样做,会抛出 std::bad_variant_access 异常或返回 nullptr
  • 空间效率: 通常会在栈上分配足够的空间来存储其所有可能类型中最大的那个,避免了动态内存分配(除非内部类型本身需要动态分配)。
  • 无额外开销: 相比于多态(虚函数),std::variant 通常没有虚函数表的开销。
  • 访问方式:
    • std::get<T>(variant_obj):按类型获取值,如果类型不匹配则抛出 std::bad_variant_access
    • std::get<index>(variant_obj):按索引获取值,如果索引不匹配则抛出 std::bad_variant_access
    • std::get_if<T>(&variant_obj):按类型获取值的指针,如果类型不匹配则返回 nullptr
    • std::visit(callable, variant_obj...):这是最强大和推荐的访问方式。它接受一个或多个 callable 对象(例如 lambda 表达式、函数对象)和一个或多个 std::variant 对象,然后根据 variant 当前持有的类型调用相应的 callable 重载。这是一种类型安全的访问模式,类似于访问者模式。
  • 空状态: 默认构造的 std::variant 会持有其第一个模板参数类型的默认构造值。如果你想表示一个“空”或“未初始化”的状态,可以使用 std::monostate 作为它的第一个类型。

何时使用 std::variant

  • 当你有一个有限且已知的类型集合,并且变量在任何时候只能是其中一个。
  • 实现状态机。
  • 函数返回多种可能的结果类型(例如 std::expected 的底层实现)。
  • 解析抽象语法树 (AST) 节点。
  • 替代类型不安全的 C 风格联合体。

示例:

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
#include <iostream>
#include <string>
#include <variant> // C++17

int main() {
// 声明一个 variant,它可以是 int, double, 或 std::string
std::variant<int, double, std::string> data;

// 赋值为 int
data = 10;
std::cout << "Data holds int: " << std::get<int>(data) << std::endl;
std::cout << "Index: " << data.index() << std::endl; // 0 (int 是第一个类型)

// 赋值为 double
data = 3.14;
std::cout << "Data holds double: " << std::get<double>(data) << std::endl;
std::cout << "Index: " << data.index() << std::endl; // 1 (double 是第二个类型)

// 赋值为 std::string
data = "Hello Variant!";
std::cout << "Data holds string: " << std::get<std::string>(data) << std::endl;
std::cout << "Index: " << data.index() << std::endl; // 2 (std::string 是第三个类型)

// 尝试获取错误的类型会抛出异常
try {
std::get<int>(data); // 当前 data 是 string,不是 int
} catch (const std::bad_variant_access& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

// 使用 std::visit 访问
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "Visited an int: " << arg * 2 << std::endl;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "Visited a double: " << arg * 2.0 << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "Visited a string: " << arg << " (length: " << arg.length() << ")" << std::endl;
}
}, data); // data 当前是 "Hello Variant!"

// 或者使用多个 variant
std::variant<int, std::string> v1 = 10;
std::variant<double, bool> v2 = true;

std::visit([](auto&& arg1, auto&& arg2){
std::cout << "Visited two variants: " << arg1 << ", " << arg2 << std::endl;
}, v1, v2); // 输出: Visited two variants: 10, 1

return 0;
}

2. std::any (任意类型)

std::any 是一个类型安全的容器,它可以存储任何单个值,只要该类型是可复制构造 (CopyConstructible) 的。你不需要在编译时知道它可能包含哪些类型,它在运行时处理类型信息。

核心思想: “这个变量可以包含任何东西,但它在任何时候都只包含一个东西。我需要在使用时猜测并验证它里面是什么。”

特点:

  • 运行时未知类型: 你不需要在声明 std::any 时指定它可能包含的类型。它可以在运行时存储任何可复制构造的类型。
  • 类型擦除 (Type Erasure): std::any 通过类型擦除技术来存储任意类型的数据。它内部维护了存储值的副本以及其类型信息。
  • 动态内存分配: 通常会涉及动态内存分配来存储其内部的值,尤其当存储的对象大小超过某个阈值时(小对象优化可能会在栈上)。
  • 运行时类型检查: 访问 std::any 中的值需要进行运行时类型检查。
  • 访问方式:
    • std::any_cast<T>(any_obj):尝试将 any_obj 中的值转换为类型 T。如果 any_obj 为空或存储的类型不是 T,则抛出 std::bad_any_cast 异常。
    • std::any_cast<T>(&any_obj):尝试将 any_obj 中的值转换为类型 T 的指针。如果 any_obj 为空或存储的类型不是 T,则返回 nullptr
    • any_obj.has_value():检查 any_obj 是否包含值。
    • any_obj.type():返回 std::type_info 对象,提供关于存储类型的信息(通常用于调试或更复杂的类型检查)。

何时使用 std::any

  • 当你需要存储真正任意的类型,并且在编译时无法预知所有可能的类型。
  • 实现插件系统,插件可以返回任意类型的数据。
  • 配置系统,配置项的值可以是各种类型。
  • 事件系统,事件携带的数据可以是任意类型。
  • 需要传递任意上下文数据到通用函数中。

示例:

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
#include <iostream>
#include <string>
#include <any> // C++17
#include <vector>

int main() {
// 声明一个 std::any
std::any value;

// 赋值为 int
value = 42;
std::cout << "Value holds int: " << std::any_cast<int>(value) << std::endl;
std::cout << "Type name: " << value.type().name() << std::endl; // 输出类似 "i" 或 "int"

// 赋值为 std::string
value = std::string("Hello Any!");
std::cout << "Value holds string: " << std::any_cast<std::string>(value) << std::endl;
std::cout << "Type name: " << value.type().name() << std::endl; // 输出类似
// 赋值为自定义类型 (必须是 CopyConstructible)
struct MyStruct {
int id;
std::string name;
MyStruct(int i, std::string n) : id(i), name(std::move(n)) {}
MyStruct(const MyStruct&) = default; // 必须是可复制构造的
MyStruct& operator=(const MyStruct&) = default;
};
value = MyStruct{1, "Test"};
MyStruct s = std::any_cast<MyStruct>(value);
std::cout << "Value holds MyStruct: " << s.id << ", " << s.name << std::endl;

// 尝试获取错误的类型会抛出异常
try {
std::any_cast<double>(value); // 当前 value 是 MyStruct
} catch (const std::bad_any_cast& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

// 使用指针版本进行安全检查
if (int* p_int = std::any_cast<int>(&value)) {
std::cout << "It's an int: " << *p_int << std::endl;
} else if (MyStruct* p_struct = std::any_cast<MyStruct>(&value)) {
std::cout << "It's a MyStruct: " << p_struct->id << std::endl;
} else {
std::cout << "It's something else or empty." << std::endl;
}

value.reset(); // 清空 any
if (!value.has_value()) {
std::cout << "Value is empty." << std::endl;
}

return 0;
}

std::variant vs std::any 总结对比

特性 std::variant std::any
类型集合 编译时已知,固定,有限 运行时未知,任意 (任何 CopyConstructible 类型)
安全性 编译时安全 (通过 std::visit 和类型列表) 运行时安全 (通过 std::any_cast 检查)
性能/开销 通常更高,无动态分配(除非内部类型大),无类型擦除开销 通常较低,涉及动态分配和类型擦除开销
内存分配 通常在栈上,大小为最大成员类型的大小 通常在堆上(对于大对象),涉及动态内存管理
访问方式 std::get, std::get_if, std::visit (推荐) std::any_cast (可能抛异常或返回 nullptr)
适用场景 有限的、已知的、固定的几种类型选择 任意的、未知的类型,需要极致的灵活性
空状态 默认持有第一个类型的值,可使用 std::monostate has_value()false 时为空,或调用 reset() 清空

简单来说:

  • 如果你知道所有可能的类型,并且这个集合是固定的,使用 std::variant。它提供了更好的编译时安全性和性能。
  • 如果你需要存储任何类型的数据,并且在编译时无法预知所有可能性,使用 std::any。它提供了最大的灵活性,但代价是运行时开销和更多的运行时类型检查。