在 C++17 之后,标准库新增了两种用于处理多类型值的容器:std::variant 和 std::any。虽然它们都能保存任意类型的数据,但两者的设计哲学、类型安全、性能开销以及使用场景各有侧重。下面将从实现原理、使用方式、类型安全、性能表现以及典型应用等方面对比两者,并给出实战建议。
1. 基本概念
std::variant |
std::any |
|
|---|---|---|
| 类型安全 | 静态类型安全,编译期确定可存储的类型列表 | 运行时类型安全,需手动检查和转换 |
| 典型用途 | 多态值、代替联合、状态机、函数参数的多种形式 | 存储任何类型的数据,类似脚本语言的“任何值” |
| 存储方式 | 内部使用联合 + 活跃成员索引 | 动态分配内存,存储对象的拷贝或移动 |
| 性能 | 对象大小固定,拷贝/移动成本可控 | 需要动态内存管理,拷贝/移动成本高 |
2. 语法与基本操作
2.1 std::variant
#include <variant>
#include <iostream>
#include <string>
using Var = std::variant<int, std::string, double>;
int main() {
Var v = 42; // 初始化为 int
std::cout << std::get<int>(v) << '\n';
v = std::string("hello"); // 赋值为 string
std::cout << std::get<std::string>(v) << '\n';
// 访问时可使用 std::visit
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
}
2.2 std::any
#include <any>
#include <iostream>
#include <string>
int main() {
std::any a = 10; // 初始化为 int
std::cout << std::any_cast<int>(a) << '\n';
a = std::string("world"); // 赋值为 string
std::cout << std::any_cast<std::string>(a) << '\n';
// 运行时检查类型
if (a.type() == typeid(std::string)) {
std::cout << "a holds a string\n";
}
}
3. 类型安全与错误处理
std::variant |
std::any |
|
|---|---|---|
| 错误捕获 | 访问错误时抛出 std::bad_variant_access |
访问错误时抛出 std::bad_any_cast |
| 运行时检查 | `std::holds_alternative | |
(v)|a.type() == typeid(T)` |
||
| 编译时约束 | 必须预先列出所有合法类型 | 任何类型均可,无编译期约束 |
- variant 通过模板参数列表明确可存储的类型,编译器可以在编译期检查类型合法性,避免不匹配的赋值。
- any 则是完全运行时决定类型,适合需要在运行时动态决定存储类型的情况,但也容易导致类型错误。
4. 性能比较
std::variant |
std::any |
|
|---|---|---|
| 内存占用 | 固定大小(最大类型大小 + 对齐) | 至少为指针大小 + 对象管理元数据 |
| 复制/移动 | O(1) 或 O(n) 取决于类型 | 需要堆分配,O(n) |
| 访问成本 | O(1) | O(1)(但涉及类型检查) |
| 对齐 | 自己管理 | 由 std::any 负责 |
- 对于需要频繁访问或复制的值,
variant更高效。 - 对于一次性存取或需要非常灵活的类型容器,
any更适合。
5. 常见应用场景
5.1 std::variant
- 状态机
用于描述有限状态集合的值,例如State = std::variant<Idle, Running, Paused>; - 函数重载实现
通过std::visit对不同类型做不同处理。 - JSON/YAML 解析
std::variant<std::nullptr_t, bool, int, double, std::string, std::vector<...>, std::map<...>> - 多值返回
当函数可能返回多种不同类型时,使用variant统一返回。
5.2 std::any
- 插件系统
插件之间需要传递任意类型的数据,使用any作为通用容器。 - 属性系统
对象属性可以是任意类型,使用any存储属性值。 - 脚本与数据绑定
需要把 C++ 对象暴露给脚本语言时,用any封装可序列化的数据。 - 临时存储
在不知道类型的情况下临时存储,后续通过typeid或any_cast再转回。
6. 组合使用的技巧
-
variant
在variant的合法类型中嵌套any,既能保证某些字段类型已知,又能在某些字段上使用任意类型。 -
多态继承 + std::variant
若对象是基类指针,可在variant中存储指向基类的 `std::shared_ptr -
std::variant 与 std::optional 的组合
std::optional<std::variant<...>>既能表示“无值”,又能容纳多种合法类型。
7. 实战示例:简单的属性系统
#include <any>
#include <unordered_map>
#include <string>
#include <iostream>
class PropertyBag {
std::unordered_map<std::string, std::any> props;
public:
template<typename T>
void set(const std::string& key, T value) {
props[key] = std::move(value);
}
template<typename T>
T get(const std::string& key) const {
auto it = props.find(key);
if (it == props.end())
throw std::runtime_error("Property not found");
return std::any_cast <T>(it->second);
}
bool contains(const std::string& key) const {
return props.find(key) != props.end();
}
};
int main() {
PropertyBag bag;
bag.set("id", 123);
bag.set("name", std::string("Alice"));
bag.set("active", true);
std::cout << "id: " << bag.get<int>("id") << '\n';
std::cout << "name: " << bag.get<std::string>("name") << '\n';
std::cout << "active: " << bag.get<bool>("active") << '\n';
}
8. 结论
std::variant:适合已知类型集合、需要类型安全、性能敏感的场景;通过std::visit可以优雅地处理多态值。std::any:适合需要在运行时决定存储类型、灵活性极高的场景;但需承担类型检查成本和内存开销。
在实际项目中,常见的做法是:在内部实现层使用 variant,在对外暴露接口时或与脚本/插件交互时使用 any。两者配合可以既保持类型安全,又兼顾灵活性。