在现代 C++(从 C++17 开始)中,std::variant 为我们提供了一种类型安全的方式来存储多种可能类型的值,类似于“和类型”(union)但更安全、更灵活。它常被用来替代传统的 std::any、boost::variant 或手写的多态实现。下面将详细介绍 std::variant 的核心概念、常用操作以及一个完整的使用示例。
1. 基础概念
1.1 什么是 std::variant
std::variant<Ts...> 是一个模板类,它接受一个或多个类型参数 Ts,在运行时只会保存其中一种类型的值。不同于 std::any 的非类型安全,它在编译期就已经确定了所有可能的类型,从而在使用时可以避免很多类型转换错误。
1.2 核心特性
| 特性 | 说明 |
|---|---|
| 类型安全 | 编译期检查,不能随意取值 |
| 可访问性 | `std::get |
()、std::get_if()` |
|
| 访问器 | std::visit 进行模式匹配 |
| 默认构造 | 只能在第一个类型上构造,或者使用 std::variant 的默认值 |
| 无拷贝 | 只在值存在时才拷贝,避免了无意义的拷贝 |
2. 常用操作
2.1 定义
std::variant<int, double, std::string> v;
2.2 赋值
v = 42; // 赋值 int
v = 3.14; // 赋值 double
v = std::string("hello"); // 赋值 std::string
2.3 查询当前类型
std::cout << "Index: " << v.index() << "\n"; // 返回当前类型索引
std::cout << "Type: " << v.type().name() << "\n"; // 返回 RTTI 类型名称
2.4 取值
try {
int i = std::get <int>(v); // 若不匹配则抛异常 std::bad_variant_access
std::cout << "int: " << i << "\n";
} catch (const std::bad_variant_access&) {
std::cout << "v 不包含 int 类型\n";
}
if (auto p = std::get_if<std::string>(&v)) {
std::cout << "string: " << *p << "\n";
}
2.5 访问器(visit)
auto visitor = [](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "int: " << arg << "\n";
else if constexpr (std::is_same_v<T, double>)
std::cout << "double: " << arg << "\n";
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "string: " << arg << "\n";
};
std::visit(visitor, v);
3. 实际应用场景
3.1 解析 JSON
使用 std::variant 可以直观地表示 JSON 的基本类型(null、bool、number、string、array、object)。例如:
using JsonValue = std::variant<
std::monostate, // null
bool,
double,
std::string,
std::vector <JsonValue>,
std::map<std::string, JsonValue>
>;
3.2 命令模式
命令对象可以定义为 std::variant,以避免在运行时使用 dynamic_cast:
struct Move { int dx, dy; };
struct Resize { int width, height; };
using Command = std::variant<Move, Resize>;
void execute(const Command& cmd) {
std::visit(overloaded {
[](const Move& m){ /* 处理移动 */ },
[](const Resize& r){ /* 处理尺寸调整 */ }
}, cmd);
}
4. 性能与注意事项
- 大小:
std::variant的大小等于最大成员类型的大小加上一个偏移量(通常是sizeof(size_t))。因此不要把大对象直接放进去,而是使用std::shared_ptr或std::unique_ptr。 - 构造与析构:只会构造/析构当前类型,避免不必要的资源管理。
- 移动语义:
std::variant默认支持移动,适合与std::move搭配使用。 - 错误处理:
std::get抛异常,std::get_if返回指针。根据需要选择。
5. 示例:实现一个简单的表达式求值器
#include <variant>
#include <string>
#include <iostream>
#include <map>
#include <memory>
#include <vector>
#include <cmath>
struct Expr {
using ExprPtr = std::shared_ptr <Expr>;
struct Add { ExprPtr left, right; };
struct Sub { ExprPtr left, right; };
struct Mul { ExprPtr left, right; };
struct Div { ExprPtr left, right; };
struct Pow { ExprPtr base, exponent; };
struct Neg { ExprPtr operand; };
struct Const { double value; };
using Value = std::variant<Add, Sub, Mul, Div, Pow, Neg, Const>;
Value value;
};
double eval(const Expr::ExprPtr& e) {
return std::visit(overloaded{
[](const Expr::Add& a){ return eval(a.left) + eval(a.right); },
[](const Expr::Sub& s){ return eval(s.left) - eval(s.right); },
[](const Expr::Mul& m){ return eval(m.left) * eval(m.right); },
[](const Expr::Div& d){ return eval(d.left) / eval(d.right); },
[](const Expr::Pow& p){ return std::pow(eval(p.base), eval(p.exponent)); },
[](const Expr::Neg& n){ return -eval(n.operand); },
[](const Expr::Const& c){ return c.value; }
}, e->value);
}
int main() {
// 计算 (3 + 4) * (2 - 1)
auto expr = std::make_shared <Expr>();
expr->value = Expr::Mul{
std::make_shared <Expr>(Expr{Expr::Add{std::make_shared<Expr>(Expr{Expr::Const{3}}),
std::make_shared <Expr>(Expr{Expr::Const{4}})}}),
std::make_shared <Expr>(Expr{Expr::Sub{std::make_shared<Expr>(Expr{Expr::Const{2}}),
std::make_shared <Expr>(Expr{Expr::Const{1}})}})
};
std::cout << "Result: " << eval(expr) << std::endl;
}
运行结果:
Result: 7
6. 小结
std::variant是一种类型安全、内存占用可控的“和类型”实现。- 与
std::any不同,它提供了编译期的类型检查。 std::visit为访问不同类型提供了优雅的模式匹配方式。- 适用于解析 JSON、实现命令模式、构造表达式树等多种场景。
掌握 std::variant 后,你可以写出更安全、更易维护的 C++ 代码,减少动态多态带来的运行时错误与性能开销。祝你编码愉快!