在 C++17 之后,标准库提供了 std::variant,允许在单个对象中存放多种类型,并在运行时保证类型安全。与传统的继承和虚函数相比,std::variant 更加轻量、无 RTTI 负担,并能在编译时做出更严格的类型检查。下面我们从基本语法、访问方式、访问者模式以及实际案例几个方面进行详细说明。
1. 基本语法与使用
1.1 声明
#include <variant>
#include <string>
std::variant<int, double, std::string> data;
这里 data 可以存放 int、double 或 std::string 三种类型中的任意一种。
1.2 赋值
data = 42; // 存放 int
data = 3.14; // 存放 double
data = std::string("C++"); // 存放 string
如果需要显式指定类型,可以使用 std::variant 的构造函数:
data = std::variant<int, double, std::string>{std::in_place_type<double>, 2.71};
1.3 查询当前类型
if (std::holds_alternative <int>(data)) {
std::cout << "int\n";
}
或使用 index() 获得类型索引(从 0 开始):
std::cout << "当前索引:" << data.index() << '\n';
2. 访问内容
2.1 std::get
int i = std::get <int>(data); // 若当前不是 int,抛出 std::bad_variant_access
double d = std::get <double>(data);
如果不确定当前类型,建议使用 std::get_if:
if (auto p = std::get_if <int>(&data)) {
std::cout << "int value: " << *p << '\n';
}
2.2 std::visit
std::visit 是访问 std::variant 的核心方式。它可以接受一个可调用对象(函数对象、lambda 等)以及一个或多个 variant,在运行时将对应类型的值作为参数传递给可调用对象。
std::visit([](auto&& arg){
std::cout << "value: " << arg << '\n';
}, data);
如果同时需要访问多个 variant,std::visit 可以接受多个参数,参数顺序与可调用对象的参数顺序对应。
3. 访问者模式的实现
通过 std::visit 可以轻松实现访问者模式。假设我们有以下几种形状:
struct Circle { double radius; };
struct Rectangle { double width, height; };
struct Triangle { double base, height; };
using Shape = std::variant<Circle, Rectangle, Triangle>;
然后定义一个访问者:
struct ShapeArea {
double operator()(const Circle& c) const {
return 3.1415926535 * c.radius * c.radius;
}
double operator()(const Rectangle& r) const {
return r.width * r.height;
}
double operator()(const Triangle& t) const {
return 0.5 * t.base * t.height;
}
};
使用方式:
Shape s = Circle{5.0};
double area = std::visit(ShapeArea{}, s);
std::cout << "Area: " << area << '\n';
这与传统的虚函数实现相比,消除了多态层的虚表开销,并在编译阶段就能检查所有形状类型是否已覆盖。
4. 复杂用法:嵌套 Variant 与自定义类型
4.1 嵌套
variant 也可以存放其他 variant,从而构建更为灵活的树状结构。
using IntOrString = std::variant<int, std::string>;
using Nested = std::variant<IntOrString, double>;
4.2 自定义类型的约束
variant 要求所有成员类型必须满足 CopyConstructible 或 MoveConstructible,但不一定需要 DefaultConstructible。如果需要自定义类型:
struct MyType {
int id;
std::string name;
};
using Var = std::variant<int, MyType>;
使用时,需要为自定义类型提供合适的构造和移动语义。
5. 性能对比
| 方案 | 编译时检查 | 运行时开销 | 内存占用 | 典型使用场景 |
|---|---|---|---|---|
| 虚函数 | ✅ | 低(虚表) | 8~16 B | 需要多态继承 |
std::variant |
✅ | 低(单一对象) | 16~32 B | 类型安全且不需要 RTTI |
union + enum |
❌ | 低 | 4~8 B | 对性能极致追求且已知类型 |
std::variant 的性能几乎与传统的 union 相当,且在使用 std::visit 时,编译器能够进行函数内联,进一步提升速度。
6. 真实项目案例:配置系统
在大型项目中,常常需要读取各种类型的配置(整数、字符串、布尔值、数组等)。下面演示如何使用 std::variant 设计一个简易的配置项。
#include <variant>
#include <string>
#include <vector>
#include <iostream>
#include <map>
using ConfigValue = std::variant<
int, double, std::string, bool, std::vector<std::string>
>;
class Config {
std::map<std::string, ConfigValue> data_;
public:
template<typename T>
void set(const std::string& key, T&& value) {
data_[key] = std::forward <T>(value);
}
template<typename T>
T get(const std::string& key) const {
auto it = data_.find(key);
if (it == data_.end()) throw std::runtime_error("Key not found");
return std::get <T>(it->second);
}
void print() const {
for (const auto& [k, v] : data_) {
std::cout << k << " = ";
std::visit([](auto&& val){ std::cout << val; }, v);
std::cout << '\n';
}
}
};
使用示例:
int main() {
Config cfg;
cfg.set("max_threads", 8);
cfg.set("app_name", "MyApp");
cfg.set("debug", true);
cfg.set("servers", std::vector<std::string>{"10.0.0.1", "10.0.0.2"});
cfg.print();
int threads = cfg.get <int>("max_threads");
std::cout << "Threads: " << threads << '\n';
}
此方案在编译时已确定各配置项的类型,且访问时类型安全,避免了传统 map<string, string> 需要手动解析的麻烦。
7. 小结
- std::variant 是 C++17 引入的类型安全、多态容器,适用于需要在运行时动态切换类型但又不想使用 RTTI 的场景。
- 通过
std::visit可以实现类似访问者模式的逻辑,减少继承层级和虚函数开销。 - 与传统
union或struct相比,std::variant提供了更完善的错误检查和更直观的接口。 - 在配置系统、事件系统、解析器等领域都有广泛的应用。
掌握 std::variant 的使用,不仅能提升代码的类型安全性,还能在不牺牲性能的前提下,获得更简洁的实现方式。