std::variant 和 std::any 都是 C++17 标准库提供的类型安全的“容器”,用来存放不同类型的值。它们在实现上类似,都可以实现“多态”,但它们的用途、语义以及使用方式有着明显区别。本文将从设计初衷、类型安全、性能以及实际应用场景四个维度,对 std::variant 与 std::any 进行对比,帮助你在实际编码中做出更合理的选择。
1. 设计初衷
std::any
- 通用性:任何类型(无论是否可拷贝、可移动)都可以存放。
- 运行时类型信息:存放的对象类型信息在运行时动态决定,访问时需要使用
any_cast。 - 轻量级:内部实现通常为“类型擦除” + 内存分配,存取时没有编译期的类型检查。
std::variant
- 固定类型集合:在声明时就确定了可接受的类型集合(如
variant<int, double, std::string>)。 - 编译期类型检查:访问时需要知道确切的类型或使用访问器(
std::get/std::visit),编译器可以保证类型安全。 - 无运行时开销:因为类型集合已知,内部实现一般是“联合体 + 活动成员索引”,没有动态分配。
2. 类型安全与访问方式
| std::any | std::variant | |
|---|---|---|
| 类型信息 | 运行时保存类型信息 | 编译期已知类型 |
| 访问方式 | `any_cast | |
或std::any_cast(需指定类型) |std::get、std::get或std::visit` |
||
| 错误处理 | 访问错误抛 bad_any_cast |
访问错误抛 bad_variant_access |
| 编译器检查 | 只能在运行时检查 | 编译时可以检测访问错误(如使用错误的索引) |
Tip:如果你想在代码中使用
switch语法遍历多种类型,std::variant的std::visit更为合适;若你需要将不同类型的对象统一存放在容器里并在运行时决定类型,std::any是更好的选择。
3. 性能比较
- 内存占用:
std::variant只需存放最大的成员尺寸加上活动索引,通常比std::any低。 - 复制与移动:
std::variant复制/移动时只调用对应类型的拷贝/移动构造,开销小;std::any复制/移动时需要进行类型擦除和动态分配,稍显昂贵。 - 访问速度:
std::variant访问时不涉及动态分配,速度更快;std::any访问需通过any_cast检查类型,存在一定开销。
4. 典型使用场景
| 需求 | 推荐类型 | 说明 |
|---|---|---|
| 需要存储多种不确定类型对象,类型决定在运行时 | std::any | 如配置文件解析、插件系统等 |
| 需要在编译期确定可接受的类型集合,且访问时需要编译期安全 | std::variant | 如解析 JSON 对象、实现状态机等 |
| 想要“模式匹配”式的访问 | std::variant + std::visit | 代码更简洁、易读 |
| 需要存储非拷贝构造/移动构造的对象 | std::any | 但需自行管理生命周期 |
| 需要容器里存放多种类型元素 | std::vector<std::variant> | 但容器元素类型固定,适合类型集合已知的情况 |
5. 实战示例
5.1 使用 std::variant 实现 JSON 解析的值类型
#include <variant>
#include <string>
#include <vector>
#include <map>
using JsonValue = std::variant<
std::nullptr_t,
bool,
double,
std::string,
std::vector <JsonValue>,
std::map<std::string, JsonValue>
>;
void printJson(const JsonValue& v, int indent = 0) {
std::visit([&](auto&& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, std::nullptr_t>) {
std::cout << "null";
} else if constexpr (std::is_same_v<T, bool>) {
std::cout << (val ? "true" : "false");
} else if constexpr (std::is_same_v<T, double>) {
std::cout << val;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << '"' << val << '"';
} else if constexpr (std::is_same_v<T, std::vector<JsonValue>>) {
std::cout << "[\n";
for (const auto& e : val) {
std::cout << std::string(indent + 2, ' ');
printJson(e, indent + 2);
std::cout << ",\n";
}
std::cout << std::string(indent, ' ') << "]";
} else if constexpr (std::is_same_v<T, std::map<std::string, JsonValue>>) {
std::cout << "{\n";
for (const auto& [k, v] : val) {
std::cout << std::string(indent + 2, ' ') << '"' << k << "\": ";
printJson(v, indent + 2);
std::cout << ",\n";
}
std::cout << std::string(indent, ' ') << "}";
}
}, v);
}
5.2 使用 std::any 处理插件系统中的不确定参数
#include <any>
#include <vector>
#include <iostream>
struct Plugin {
void (*execute)(std::vector<std::any>& args);
};
void fooPlugin(std::vector<std::any>& args) {
// 假设插件需要 int 与 std::string
int n = std::any_cast <int>(args[0]);
std::string msg = std::any_cast<std::string>(args[1]);
std::cout << "fooPlugin: " << n << " - " << msg << '\n';
}
int main() {
Plugin p{fooPlugin};
std::vector<std::any> params;
params.emplace_back(42);
params.emplace_back(std::string("hello"));
p.execute(params); // 运行时根据 std::any 访问参数
}
6. 小结
- std::any:灵活、通用、运行时类型决定;适合插件、配置等动态类型场景。
- std::variant:类型集合固定、编译期安全、性能更佳;适合需要“模式匹配”或状态机等场景。
在实际项目中,先评估“类型是否固定”,再决定使用哪个容器。若你需要把“任何类型”放进一个统一容器,使用 std::any;若你已经确定了可接受的类型集合,并想要编译期检查,std::variant 是更合适的选择。祝你编码愉快!