在 C++17 之前,处理不同类型数据的常用方法是使用 void*、继承或手写多态,但这些方法往往缺乏类型安全或代码冗长。C++17 引入了两种新的类型擦除容器:std::variant 与 std::any。它们虽然都可以存储任意类型,但在语义、使用场景和性能方面有着明显差异。
1. 基本概念
-
std::variant
代表一组预先确定的类型中的任意一种。编译器在编译期知道所有可能的类型,并对其进行联合存储。访问时需要通过std::get、std::visit或index()等机制获取当前类型,并强制转换为具体类型。 -
std::any
` 获取指定类型的引用,若类型不匹配会抛出异常。
代表任意类型的数据,完全在运行时决定。std::any通过类型擦除(type erasure)实现,对任何可拷贝或可移动的类型都能存储。访问时通过 `std::any_cast
2. 主要区别
| 特点 | std::variant | std::any |
|---|---|---|
| 类型列表 | 编译期固定 | 运行时动态 |
| 内存布局 | 单一联合 + 活跃索引 | 通过 heap 或 small-buffer optimization |
| 访问方式 | std::visit、index、std::get | std::any_cast |
| 类型安全 | 编译时检查 | 运行时检查 |
| 性能 | 较快,避免 heap 分配 | 可能涉及堆分配,存在性能损耗 |
| 用法 | 多态替代、状态机 | 需要统一存储不确定类型的接口 |
3. 使用场景
3.1 std::variant
- 状态机:一个状态变量只会在有限几种类型之间切换。
using State = std::variant<InitState, RunningState, ErrorState>; State current; - 函数返回多种可能类型:返回值既可能是错误码,也可能是成功结果。
std::variant<std::string, std::error_code> parse(const std::string& s); - 配置参数:某个配置值可为 int、double 或 std::string,但只能是这些类型之一。
using ConfigValue = std::variant<int, double, std::string>;
3.2 std::any
- 插件系统:插件提供任意类型的数据,主程序不需要预先知道。
std::any data = loadPluginData(); // 可能是 std::vector <int> 或 MyCustomType - 通用事件系统:事件携带不同类型的负载。
struct Event { std::string type; std::any payload; }; - 临时缓存:在不想定义多个容器的情况下,暂存任意类型的临时数据。
4. 示例代码
4.1 variant 示例:状态机
#include <variant>
#include <iostream>
#include <string>
struct InitState { std::string msg = "init"; };
struct RunningState { int counter = 0; };
struct ErrorState { std::string err; };
using State = std::variant<InitState, RunningState, ErrorState>;
void process(State& s) {
std::visit(overloaded{
[](InitState& st){ std::cout << "Init: " << st.msg << '\n'; },
[](RunningState& st){ std::cout << "Running: " << st.counter++ << '\n'; },
[](ErrorState& st){ std::cout << "Error: " << st.err << '\n'; }
}, s);
}
// Helper for overloaded lambdas
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
4.2 any 示例:事件系统
#include <any>
#include <iostream>
#include <string>
#include <vector>
struct Event {
std::string type;
std::any payload;
};
void dispatch(const Event& e) {
if (e.type == "numbers") {
try {
const auto& vec = std::any_cast<const std::vector<int>&>(e.payload);
for (int n : vec) std::cout << n << ' ';
std::cout << '\n';
} catch (const std::bad_any_cast&) {
std::cerr << "Bad cast for numbers event\n";
}
} else if (e.type == "message") {
try {
std::cout << std::any_cast<std::string>(e.payload) << '\n';
} catch (const std::bad_any_cast&) {
std::cerr << "Bad cast for message event\n";
}
}
}
5. 性能对比
- variant:所有成员类型已在编译期确定,内存可以在栈上布局,避免堆分配,访问通过
index或std::visit进行分支判断,性能优于any。 - any:需要动态分配内存(或使用 small buffer optimization),并在访问时进行类型检查,产生额外开销。适合类型不确定、数量极少或对性能要求不高的场景。
6. 小结
- 当你已知所有可能类型且数量有限时,首选
std::variant。 - 当类型完全未知、或者需要统一接口来存储任意类型时,使用
std::any。 - 两者都提供了更安全、更简洁的方式来代替传统的
void*或手写多态方案。正确选型能让代码更易读、维护成本更低,也能提升运行时性能。