113 lines
4.3 KiB
Markdown
113 lines
4.3 KiB
Markdown
`std::variant` 是 C++17 标准库引入的一个非常有用的模板类,定义在 `<variant>` 头文件中。
|
||
|
||
用一句话概括:**它是 C++ 中的“类型安全的联合体 (Type-safe Union)”。**
|
||
|
||
### 1. 核心概念
|
||
|
||
你可以把 `std::variant<A, B, C>` 想象成一个盒子,这个盒子在任何时刻**只能**装着 A、B 或 C 类型中的**某一个**值。
|
||
|
||
* **对比 `struct`/`pair`**:结构体同时持有所有成员(A **和** B **和** C)。
|
||
* **对比 `union`**:传统的 C 语言 `union` 也可以只持有一个值,但它不知道当前存的是什么类型,容易出错(类型不安全)。
|
||
* **对比 `std::variant`**:它知道自己当前存的是什么类型,并且会自动管理内存(比如调用析构函数),非常安全。
|
||
|
||
### 2. 基本用法
|
||
|
||
#### 定义和赋值
|
||
```cpp
|
||
#include <iostream>
|
||
#include <variant>
|
||
#include <string>
|
||
|
||
int main() {
|
||
// 定义一个 variant,它可以是 int, double, 或者 string
|
||
std::variant<int, double, std::string> v;
|
||
|
||
v = 10; // 现在 v 存的是 int
|
||
v = 3.14; // 现在 v 存的是 double
|
||
v = "Hello World"; // 现在 v 存的是 string
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### 获取值 (`std::get`)
|
||
你需要明确告诉编译器你想取什么类型的值。
|
||
|
||
```cpp
|
||
v = 100;
|
||
|
||
// 正确:当前存的是 int
|
||
int i = std::get<int>(v);
|
||
// 或者通过索引获取(0代表第一个类型 int)
|
||
int i2 = std::get<0>(v);
|
||
|
||
try {
|
||
// 错误:当前存的是 int,但你试图取 string
|
||
std::string s = std::get<std::string>(v);
|
||
} catch (const std::bad_variant_access& e) {
|
||
std::cout << "类型取错了: " << e.what() << std::endl;
|
||
}
|
||
```
|
||
|
||
#### 安全检查 (`std::holds_alternative` 和 `std::get_if`)
|
||
为了避免抛出异常,通常会先检查或使用指针方式获取。
|
||
|
||
```cpp
|
||
v = 3.14;
|
||
|
||
// 方法1:先检查类型
|
||
if (std::holds_alternative<double>(v)) {
|
||
std::cout << "里面是 double: " << std::get<double>(v) << std::endl;
|
||
}
|
||
|
||
// 方法2:使用 get_if (推荐,类似于 dynamic_cast)
|
||
if (auto* p = std::get_if<double>(&v)) { // 传入地址
|
||
std::cout << "里面是 double: " << *p << std::endl;
|
||
} else {
|
||
std::cout << "里面不是 double" << std::endl;
|
||
}
|
||
```
|
||
|
||
### 3. 高级用法:`std::visit`
|
||
|
||
这是 `std::variant` 最强大的地方。如果你不想写一堆 `if-else` 来判断当前是什么类型,可以使用 `std::visit` 来根据当前存储的类型自动调用对应的处理逻辑。
|
||
|
||
```cpp
|
||
#include <variant>
|
||
#include <iostream>
|
||
|
||
int main() {
|
||
std::variant<int, double, std::string> v = "C++";
|
||
|
||
// 使用 lambda 表达式处理所有可能的情况
|
||
std::visit([](auto&& arg) {
|
||
using T = std::decay_t<decltype(arg)>;
|
||
if constexpr (std::is_same_v<T, int>)
|
||
std::cout << "它是 int: " << arg << std::endl;
|
||
else if constexpr (std::is_same_v<T, double>)
|
||
std::cout << "它是 double: " << arg << std::endl;
|
||
else if constexpr (std::is_same_v<T, std::string>)
|
||
std::cout << "它是 string: " << arg << std::endl;
|
||
}, v);
|
||
}
|
||
```
|
||
*注:在实际工程中,通常会结合 `overloaded` 模式(一种辅助结构体)来为每种类型写单独的 lambda 函数,代码会更清晰。*
|
||
|
||
### 4. 为什么要用它?(应用场景)
|
||
|
||
1. **解析异构数据**:比如解析 JSON 或配置文件,一个字段可能是数字,也可能是字符串。
|
||
2. **错误处理 (Result Type)**:
|
||
* 函数可能返回“成功的结果”或者“错误码”。
|
||
* `std::variant<ResultData, ErrorCode>` 是非常优雅的实现方式。
|
||
3. **状态机**:
|
||
* 如果一个对象有多个状态,且每个状态包含的数据不同。
|
||
* `std::variant<StateA, StateB, StateC>` 可以完美表示当前状态。
|
||
4. **替代虚函数多态**:
|
||
* 如果你有一组已知类型,使用 `std::variant` + `std::visit` 通常比继承 + 虚函数更快(没有虚表开销,内存更紧凑)。
|
||
|
||
### 5. 总结
|
||
|
||
* **是什么**:C++17 的类型安全联合体。
|
||
* **能存啥**:定义时指定的一组类型中的某一个。
|
||
* **内存**:在栈上分配(通常大小等于最大成员的大小 + 一个索引标记),没有额外的堆内存分配(除非成员本身分配堆内存,如 `std::string`)。
|
||
* **优点**:类型安全、性能高、无需手动管理生命周期。 |