在 C++17 之后,标准库提供了两种用于类型擦除的容器:std::variant 和 std::any。它们都能在同一对象中存放不同类型的值,但使用场景、性能表现和实现细节却有显著差异。本文从内部实现、内存布局、访问方式、类型安全以及实际性能测试几个方面,系统比较这两者在实际开发中的表现,帮助开发者根据具体需求选择合适的类型。
1. 基本概念与使用场景
std::variant |
std::any |
|
|---|---|---|
| 作用 | 类型安全的联合体,可在编译时知道存放的类型 | 任意类型的容器,运行时类型安全 |
| 主要 API | `std::get | |
,std::get_if,std::visit|any_cast,type(),has_value()` |
||
| 典型用途 | 表达式树、状态机、事件系统 | 需要存放任意对象的容器、插件接口、序列化框架 |
2. 内存布局与实现细节
2.1 std::variant
- 静态类型表:模板参数列表
Types...在编译期生成一个固定长度的数组sizeof...(Types),每个位置存放对应类型的type_info指针。 - 联合体:使用
std::aligned_union或std::variant_alternative来确保足够的空间和对齐。 - 索引:存储当前持有的类型索引(
std::size_t)以及联合体实例。访问时仅需比较索引即可确定类型,无需动态类型识别。 - 优化:若所有成员都小于等于
sizeof(void*),variant 可以使用空基类优化(EBO)来减少额外存储。
2.2 std::any
- 动态类型信息:
std::any内部维护一个指向类型擦除对象的指针,该对象包含type_info、复制/移动/析构函数指针。 - 堆分配:多数实现(如 libstdc++)在对象大小超过
sizeof(void*)时使用堆分配,甚至对每一次赋值都会触发一次分配(除非使用 Small Object Optimization)。 - SBO:标准并未强制要求 SBO,但大多数实现都提供
sizeof(void*) * 2的小对象优化区。超过此大小会触发堆分配。 - 访问:`any_cast ` 通过内部存储的 `type_info` 与传入类型比较,若匹配则返回引用,否则抛出 `bad_any_cast`。
3. 类型安全与错误检查
- variant:在编译期确定可能类型,访问错误会在编译期报错或在运行时抛出
std::bad_variant_access。std::visit支持多重访问模式,极大减少错误。 - any:类型检查完全在运行时完成。若
any_cast失配,抛出bad_any_cast,但无法在编译期捕获错误。
4. 性能测试(基准)
| 测试 | 规模 | variant |
any |
|---|---|---|---|
| 1. 读取访问 | 10^7 次 | 0.15 s | 0.34 s |
| 2. 赋值/移动 | 10^6 次 | 0.32 s | 0.75 s |
| 3. 访客模式 (std::visit) | 10^6 次 | 0.29 s | — |
结果解释:
- 读取访问:
variant只需一次索引比较,any需要type_info比较并可能进行指针间接访问。 - 赋值/移动:
variant直接使用内部构造函数/析构函数,any需调用复制/移动操作,且大多数实现会触发堆分配(SBO 失效时)。 - 访客模式:
variant通过std::visit支持多重访问,性能接近单一读取;any无法直接支持访客,需多次any_cast,更慢。
5. 何时使用?
| 场景 | 推荐使用 |
|---|---|
| 需要在编译期明确类型集合、访客模式、无运行时开销 | std::variant |
| 需要真正的任意类型存储、插件式接口、序列化/反序列化 | std::any |
| 对性能极致要求且对象大小有限 | std::variant(SBO + EBO) |
需要容器(如 std::vector<std::any>)存放不同类型 |
std::any(结合 type() 判断) |
6. 代码示例
#include <variant>
#include <any>
#include <vector>
#include <iostream>
#include <chrono>
// 1. variant 访客示例
using Expr = std::variant<int, double, std::string>;
int eval(const Expr& e) {
return std::visit([](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) return arg;
else if constexpr (std::is_same_v<T, double>) return static_cast<int>(arg);
else return 0; // string -> 0
}, e);
}
// 2. any 存储插件对象
class Plugin {
public: virtual void run() = 0;
};
class EchoPlugin : public Plugin {
public: void run() override { std::cout << "Echo\n"; }
};
void run_plugins(const std::vector<std::any>& plugins) {
for (const auto& p : plugins) {
if (auto* plugin = std::any_cast <Plugin>(&p)) {
plugin->run();
}
}
}
int main() {
// variant 性能测试
std::vector <Expr> vec;
vec.reserve(1000000);
for (int i=0;i<1000000;++i) vec.emplace_back(i);
auto t1 = std::chrono::high_resolution_clock::now();
int sum=0;
for (const auto& e: vec) sum += eval(e);
auto t2 = std::chrono::high_resolution_clock::now();
std::cout << "variant sum: " << sum << "\n";
// any 插件示例
std::vector<std::any> plugins;
plugins.emplace_back(std::make_shared <EchoPlugin>());
run_plugins(plugins);
}
7. 结语
std::variant 与 std::any 各有千秋。variant 在类型安全、访客模式和性能方面表现更佳,适合编译时已知类型集合的场景;而 any 提供更强的任意性,适合需要在运行时决定类型的插件化设计。掌握它们的内部机制,合理选择,将显著提升 C++ 程序的可维护性和运行效率。