在 C++17 之前,std::any 用于存储任何类型的值,但它在运行时并不知道具体的类型,导致访问时需要显式的类型检查和转换。std::variant 是一种类型安全的多态容器,它在编译时就确定了所有可能的类型,并通过索引或访问器访问相应的值。两者虽然都可以存储“任意”类型,但在使用方式、性能、语义以及安全性上存在显著差异。下面从概念、实现细节、性能、语义以及典型场景四个角度来对比这两种容器,并给出实际代码示例。
1. 概念区别
| std::any | std::variant | |
|---|---|---|
| 定义 | 存储任意类型的对象,类型信息通过 RTTI 保存 | 存储一组预定义类型中的任意一种,类型信息通过编译期元组维护 |
| 类型安全 | 运行时检查(使用 any_cast) |
编译时类型安全(`std::get |
| ` 必须与声明的类型一致) | ||
| 存储方式 | 通过 type-erasure 抽象 | 通过联合体(union)+ 标记(index)实现 |
| 可用性 | 仅限 C++17 起 | 仅限 C++17 起 |
2. 实现细节
std::any
std::any 内部实现类似:
class any {
struct placeholder {
virtual ~placeholder() = default;
virtual placeholder* clone() const = 0;
virtual const std::type_info& type() const = 0;
};
template<typename T>
struct holder : placeholder {
T value;
explicit holder(T&& v) : value(std::forward <T>(v)) {}
placeholder* clone() const override { return new holder(value); }
const std::type_info& type() const override { return typeid(T); }
};
std::unique_ptr <placeholder> content;
public:
template<typename T>
any(T&& v) : content(new holder<std::decay_t<T>>(std::forward<T>(v))) {}
// ...
};
- 采用
type-erasure:所有类型共享同一基类,真正的数据保存在派生类 `holder ` 中。 - 每次拷贝都需要
clone(),实现了深拷贝。
std::variant
实现方式:
template<typename... Ts>
class variant {
static constexpr std::size_t size_ = sizeof...(Ts);
std::aligned_union_t<0, Ts...> storage;
std::size_t index;
template<std::size_t I, typename T>
static void destroy_at() {
reinterpret_cast<T*>(&storage)->~T();
}
// ... 访问、赋值等
};
- 通过
aligned_union_t预留足够的空间来存放任意类型。 index用于记录当前存储的类型索引。- 只需要在构造/赋值时执行一次构造/析构,拷贝时也只拷贝对应类型的对象。
3. 性能比较
| std::any | std::variant | |
|---|---|---|
| 拷贝 | 需要虚函数调用 + heap 分配(默认实现) | 只做一次构造拷贝,无虚函数 |
| 访问 | 运行时 typeid + dynamic_cast 或 any_cast |
编译时索引 + 静态访问,性能更好 |
| 内存 | 需要存储类型信息(std::type_info 指针) |
只存储索引(size_t) |
| 线程安全 | 对单个 any 对象的拷贝/赋值不是线程安全的 |
同上,内部实现同样非线程安全 |
经验总结:如果你需要经常拷贝、访问,并且类型集合已知且有限,std::variant 更高效;如果类型不确定或需要真正的 “任意类型” 存储(比如动态插件系统),std::any 更合适。
4. 语义差异
- 类型检查:
std::variant在编译期就能判断你请求的类型是否存在,编译错误;std::any需要在运行时检查,否则会抛出bad_any_cast。 - 访问方式:
std::variant支持 `std::get ()`、`std::visit`、`index()` 等;`std::any` 仅支持 `any_cast`,没有直接的索引或多态访问方式。 - 异常安全:
std::variant的构造/析构遵循 RAII,异常不会导致资源泄漏;std::any由于使用 virtual base 需要仔细处理析构。
5. 典型应用场景
| 场景 | 推荐使用 |
|---|---|
| 1. 需要存储多种已知类型的数据结构(如 AST 节点) | std::variant |
| 2. 需要统一接口来传递任意用户自定义类型(如事件系统) | std::any |
| 3. 实现“自定义属性表”或“元数据容器” | 视情况而定;若属性类型多且可预知,则 variant 更合适;若属性来源多变,则 any |
| 4. 需要高性能、频繁访问的多态容器 | std::variant |
| 5. 与第三方库交互,要求兼容多种返回值 | 根据第三方提供的 API 选择 |
6. 代码示例
6.1 std::variant 的简单用法
#include <variant>
#include <iostream>
#include <string>
int main() {
std::variant<int, double, std::string> v;
v = 42; // 赋值 int
std::visit([](auto&& arg) { std::cout << arg << '\n'; }, v);
v = 3.14; // 赋值 double
std::visit([](auto&& arg) { std::cout << arg << '\n'; }, v);
v = std::string("hello"); // 赋值 string
std::visit([](auto&& arg) { std::cout << arg << '\n'; }, v);
}
6.2 std::any 的简单用法
#include <any>
#include <iostream>
#include <string>
int main() {
std::any a = 10;
std::cout << std::any_cast<int>(a) << '\n';
a = std::string("world");
std::cout << std::any_cast<std::string>(a) << '\n';
}
7. 小结
- std::variant:类型安全、性能好、适合已知类型集合;编译期确定类型,访问更安全。
- std::any:类型不确定、实现简单;运行时类型检查,性能略逊,但更灵活。
在实际项目中,建议先评估需求类型是否可预知。如果可以,优先使用 std::variant;否则才考虑 std::any。这两者在 C++17 之后成为标准容器,具备良好的可移植性和成熟的实现。