在 C++17 之前,处理多种可能的数据类型往往依赖于传统的多态(继承+虚函数)或手写的联合体和标记枚举。C++17 引入了 std::variant,它是一种类型安全的联合体,可以存放多种指定类型中的任意一种,并通过 std::visit 进行访问。本文将深入探讨 std::variant 的核心概念、典型用法、性能考量以及常见陷阱,并给出一段完整的示例代码,帮助你快速上手。
1. 为什么需要 std::variant?
-
类型安全
与union不同,std::variant会在编译时保证只能存放指定类型,且访问时必须先确定当前实际类型,避免了未定义行为。 -
无需继承
传统多态需要定义基类和派生类,使用时还要管理指针或引用。std::variant直接在栈上保存数据,消除指针相关的开销和生命周期管理。 -
与 std::visit 搭配
std::visit可以像std::visit([](auto&& v){...})这样的语法,使访问变得直观且可扩展。
2. 基本语法
#include <variant>
#include <iostream>
#include <string>
using MyVariant = std::variant<int, double, std::string>;
int main() {
MyVariant v = 42; // 存放 int
v = std::string("hello"); // 再存放 std::string
std::visit([](auto&& val){ std::cout << val << std::endl; }, v);
}
variant<Ts...>:模板参数列表定义了允许的类型集合。std::visit(visitor, variant):visitor 可以是函数对象、lambda、或者std::function。- `get ()` / `get_if()`:直接获取当前类型,若不匹配则抛出 `std::bad_variant_access` 或返回 `nullptr`。
3. 典型使用场景
3.1 表示 JSON 字段
using JsonValue = std::variant<
std::nullptr_t, bool, int64_t, double,
std::string, std::vector <JsonValue>, std::map<std::string, JsonValue>
>;
3.2 结果包装
template<typename T, typename E>
using Result = std::variant<T, E>;
与传统 std::pair<T, E> 或自定义 Either 结构类似,但更安全。
3.3 事件系统
struct ClickEvent { int x, y; };
struct KeyEvent { int keycode; };
using Event = std::variant<ClickEvent, KeyEvent>;
4. 访问技巧
4.1 单一类型访问
int getInt(const MyVariant& v) {
if (auto p = std::get_if <int>(&v))
return *p;
throw std::runtime_error("Variant does not hold int");
}
4.2 访问所有可能类型
std::visit(overloaded{
[](int i) { std::cout << "int: " << i << '\n'; },
[](double d){ std::cout << "double: " << d << '\n'; },
[](const std::string& s){ std::cout << "string: " << s << '\n'; }
}, v);
overloaded 是一个辅助模板,用于将多个 lambda 合并成一个可调用对象。
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
4.3 访问并返回值
auto getLength(const MyVariant& v) {
return std::visit([](auto&& val) -> size_t {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, std::string>)
return val.size();
else
return 0;
}, v);
}
利用 if constexpr 可以在编译期根据类型做分支。
5. 性能与实现细节
- 内部存储:
std::variant内部维护一个char buffer[max_size](对齐),以及一个index_type表示当前类型。max_size是所有类型中大小最大的一个,max_align是最大对齐。 - 移动/拷贝:默认拷贝/移动构造/赋值会调用对应类型的拷贝/移动构造函数。若所有类型都满足
MoveConstructible,则std::variant也满足。 - 大小:对于常见的
int, double, std::string组合,variant的大小大约是sizeof(void*) * 2(64 位平台),因为std::string采用小字符串优化(SSO)。 - 分支预测:
std::visit通过index对应的switch或constexpr递归实现,编译器通常能很好地优化。 - 可变形参数:如果需要在运行时动态决定类型,可以考虑
std::any或boost::variant,但std::variant受限于模板参数列表是编译时固定的。
6. 常见错误与陷阱
-
**忘记包含 `
`** 这是最常见的编译错误,尤其在使用旧编译器时。 -
错误的类型顺序
std::variant的构造/赋值优先匹配第一个相同类型。如果有同名类、别名等,可能导致意外匹配。 -
未处理的类型
std::visit需要覆盖所有可能的类型,否则在运行时抛std::bad_variant_access。使用overloaded时,可通过static_assert确保覆盖完整。 -
复制构造时不匹配
当variant的index与目标variant不同,拷贝/移动构造会失败。请确保目标variant的类型集合与源相同。 -
递归
variant的限制
variant不能直接包含自身,除非使用std::shared_ptr或std::unique_ptr进行包装。
7. 完整示例:一个简易的数学表达式求值器
#include <variant>
#include <vector>
#include <string>
#include <iostream>
#include <stdexcept>
#include <functional>
// 1. 定义表达式节点
struct Number {
double value;
};
struct BinaryOp {
char op; // '+', '-', '*', '/'
std::shared_ptr <void> left;
std::shared_ptr <void> right;
};
using ExprNode = std::variant<Number, BinaryOp>;
using Expr = std::shared_ptr <ExprNode>;
// 2. 构造表达式 (3 + 4) * 5
Expr makeSampleExpr() {
auto left = std::make_shared <ExprNode>(BinaryOp{
'+',
std::make_shared <ExprNode>(Number{3}),
std::make_shared <ExprNode>(Number{4})
});
return std::make_shared <ExprNode>(BinaryOp{
'*',
left,
std::make_shared <ExprNode>(Number{5})
});
}
// 3. 递归求值
double eval(const Expr& expr) {
return std::visit(overloaded{
[](const Number& n) { return n.value; },
[](const BinaryOp& op) {
double l = eval(std::static_pointer_cast <ExprNode>(op.left));
double r = eval(std::static_pointer_cast <ExprNode>(op.right));
switch (op.op) {
case '+': return l + r;
case '-': return l - r;
case '*': return l * r;
case '/': return l / r;
default: throw std::runtime_error("未知运算符");
}
}
}, *expr);
}
int main() {
Expr expr = makeSampleExpr();
std::cout << "Result: " << eval(expr) << std::endl; // 输出 35
}
说明
ExprNode是variant,可以存放Number或BinaryOp。BinaryOp的left、right是 `shared_ptr `,为了示例简化了类型推断。eval通过std::visit对每种节点类型做不同处理,实现递归求值。
8. 结语
std::variant 与 std::visit 的组合为 C++ 提供了强大且类型安全的多态方案。无论是 JSON 解析、事件系统、结果包装,还是简易脚本引擎,都能轻松实现。掌握它的核心概念和常用技巧,能显著提升代码的可读性与安全性。希望本文能帮助你在实际项目中快速运用 std::variant,让代码更简洁、错误更少。祝编码愉快!