C++17 新特性:std::variant —— 类型安全的多类型容器
在 C++17 之前,如果我们希望让一个变量能存储多种类型的数据(例如有时是 int,有时是 std::string),往往需要依赖 union、void* 或多态指针,但这些方式都有明显的安全隐患。
C++17 引入的 std::variant 则提供了一种更优雅、更安全的解决方案。
它是一个 类型安全的联合体(type-safe union),允许你定义一个变量,在同一时间只存储多种指定类型之一。
一、基本用法
#include <iostream>
#include <variant>
int main()
{
std::variant<std::string, int> data;
data = "Miku";
std::cout << std::get<std::string>(data) << "\n";
data = 39;
std::cout << std::get<int>(data) << "\n";
}
在上面的例子中:
-
data可以存储std::string或int -
std::get<T>(variant)用于从 variant 中取出指定类型的值
输出:
Miku
39
如果类型取错,比如当前存的是 int 却用 std::get<std::string>(data),
程序会抛出异常:
throw std::bad_variant_access;
二、判断当前存储的类型
1. 使用 .index()
variant.index() 返回当前存储类型的索引(从 0 开始):
std::variant<std::string, int> data = "Miku";
std::cout << data.index() << std::endl; // 输出 0
data = 39;
std::cout << data.index() << std::endl; // 输出 1
这种方式能快速判断类型,但不够直观,也不具备编译期安全性。
2. 使用 std::get_if
更推荐的方式是使用 std::get_if<T>():
它返回一个指向值的指针,如果类型不匹配,则返回 nullptr。
if (auto value = std::get_if<std::string>(&data))
{
std::cout << "String value: " << *value << std::endl;
}
else if (auto value = std::get_if<int>(&data))
{
std::cout << "Int value: " << *value << std::endl;
}
这种写法不会抛出异常,而且非常直观、类型安全。
三、std::visit —— 更优雅的多类型访问方式
如果 variant 包含多个可能类型,而你希望统一处理所有类型,
使用 std::visit 会比多层 if 更简洁:
std::variant<std::string, int> data = 39;
std::visit([](auto&& value) {
std::cout << "Value: " << value << std::endl;
}, data);
输出:
Value: 39
这里使用了泛型 Lambda 表达式(auto&& 参数),
编译器会根据 variant 当前类型自动推导出 value 的类型。
四、默认构造与 std::monostate
如果你定义的 variant 没有默认构造的类型,比如:
std::variant<int, std::string> v;
这时它会自动构造为第一个类型的默认值(即 int(0))。
但有时我们希望“什么都没有”,类似于“空状态”。
这时可以加上 std::monostate:
std::variant<std::monostate, int, std::string> data;
std::cout << data.index() << std::endl; // 输出 0(表示空状态)
data = "hello";
std::cout << data.index() << std::endl; // 输出 2
std::monostate 是一个占位类型,表示“当前没有实际值”。
五、std::variant 与 union 的区别
| 特性 | union |
std::variant |
|---|---|---|
| 类型安全 | ❌无类型检查,可能访问错误类型 | ✔ 自动记录当前类型 |
| 存储结构 | 所有类型共用一块内存 | 存储最大类型并记录索引 |
| 可用类型 | 限制多(不能有非平凡构造的类型) | 任意可复制/可移动类型 |
| 错误行为 | 访问错误类型是 UB(未定义行为) | 抛出 std::bad_variant_access |
| 用途 | 底层优化、驱动、嵌入式 | 安全地表达“多种可能的类型” |
简单来说:
union更快,但危险。std::variant稍慢,但几乎不可能出错。
六、实际应用:函数返回多种结果类型
std::variant 在函数返回值场景中非常常见。
比如你想实现一个函数,它在读取文件时:
-
成功则返回字符串内容;
-
失败则返回错误码。
可以这样写:
#include <variant>
#include <string>
enum class ErrorCode
{
None = 0,
NotFound = 1,
NoAccess = 2
};
std::variant<std::string, ErrorCode> ReadFileAsString(const std::string& filePath)
{
// 模拟文件读取失败
return ErrorCode::NotFound;
}
调用:
auto result = ReadFileAsString("data.txt");
std::visit([](auto&& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, std::string>)
std::cout << "File content: " << value << std::endl;
else
std::cout << "Error code: " << static_cast<int>(value) << std::endl;
}, result);
输出:
Error code: 1
这种写法不仅优雅,还能在编译期检查所有可能分支是否都被处理。
七、内存布局与性能
std::variant 的大小 ≈ 最大成员类型的大小 + 类型索引(discriminator)。
这个索引用来标记当前储存的是哪种类型。
例如:
std::variant<int, double, std::string> v;
std::cout << sizeof(v) << std::endl;
你会发现它略大于 std::string,因为内部需要额外存储当前类型索引和对齐信息。
八、总结
std::variant 是 C++17 引入的一个极其强大的类型工具,
它本质上是 受控的多态,让你能安全、清晰地表达“一个对象可能是几种类型之一”的概念。
核心特性回顾:
| 操作 | 功能 |
|---|---|
std::variant<T1, T2, ...> |
定义多类型容器 |
std::get<T>(v) |
获取指定类型的值(类型不匹配则抛异常) |
std::get_if<T>(&v) |
获取指定类型的指针(类型不匹配返回空) |
v.index() |
当前存储类型索引 |
std::visit(f, v) |
对当前存储类型执行函数 |
std::monostate |
定义“空状态” |
std::bad_variant_access |
类型错误访问异常 |
更多推荐



所有评论(0)