在C++17中,std::variant 为我们提供了一种安全且高效的方式来存储多种类型的值,并在运行时能够安全地访问它们。相比传统的多态实现(如继承与虚函数),std::variant 让我们可以在一个类中直接表达“这可以是几种类型中的任意一种”,并在编译期保留类型信息。下面将通过一个完整的示例来演示如何使用 std::variant 实现一个类型安全的多态结构,并说明其优缺点。
1. 基本语法
#include <variant>
#include <iostream>
#include <string>
using Variant = std::variant<int, double, std::string>;
int main() {
Variant v = 42; // 存储 int
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v); // 输出 42
v = 3.14; // 存储 double
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v); // 输出 3.14
v = std::string("hello"); // 存储 std::string
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v); // 输出 hello
}
std::visit 接受一个可调用对象(lambda、函数对象等),并将 variant 当前持有的值传递给它。由于 C++17 的模板推断,visit 的参数会自动匹配正确的类型。
2. 创建一个多态的“Shape”系统
传统多态示例:
struct Shape { virtual void draw() const = 0; };
struct Circle : Shape { void draw() const override { /* ... */ } };
struct Square : Shape { void draw() const override { /* ... */ } };
使用 variant 的版本:
#include <variant>
#include <iostream>
struct Circle {
void draw() const { std::cout << "Circle\n"; }
};
struct Square {
void draw() const { std::cout << "Square\n"; }
};
using Shape = std::variant<Circle, Square>;
void drawShape(const Shape& s) {
std::visit([](auto&& shape){ shape.draw(); }, s);
}
调用方式:
Shape s1 = Circle{};
Shape s2 = Square{};
drawShape(s1); // 输出 Circle
drawShape(s2); // 输出 Square
2.1 访问特定类型
如果你需要访问 variant 的具体类型,可以使用 std::get_if:
if (auto* c = std::get_if <Circle>(&s1)) {
// c 是 Circle*
c->draw();
}
3. 深入理解 std::variant
3.1 类型安全
- 编译期检查:只能存储预先声明的类型列表中的一种。
- 访问错误:`std::get ` 在类型不匹配时会抛出 `std::bad_variant_access`。`std::get_if` 通过返回 `nullptr` 让错误更安全。
3.2 性能
variant内部通常是一个 union + 一个索引(类似std::discriminated_union)。它的内存占用等价于存储最大成员的尺寸加上索引。- 访问
visit需要一个虚拟表的跳转,但其开销与普通函数指针相近。对小型、频繁使用的系统来说,性能几乎没有区别。
3.3 与继承的对比
| 方面 | 传统多态(继承) | std::variant |
|---|---|---|
| 内存 | 每个对象包含虚函数表指针(8/16 字节) | 只存储最大成员 + 索引 |
| 类型安全 | 需要手动检查 dynamic_cast 或 RTTI |
编译期保证 |
| 维护 | 难以统一添加新类型 | 简单扩展 variant 模板参数 |
| 适用 | 需要真正的“对象行为” | 需要简单的“值”多态 |
4. 典型使用场景
- 配置系统:键值对中值可以是整数、字符串、布尔等多种类型。使用
variant能让解析后的值保持类型安全。 - 事件系统:不同事件携带不同的数据。使用
variant可避免大量void*或std::any的使用。 - 树形结构:如表达式树,每个节点可以是数值、变量或运算符。使用
variant可以让节点类型更加明确。
5. 完整示例:一个简单的表达式求值器
#include <variant>
#include <string>
#include <iostream>
#include <unordered_map>
#include <stdexcept>
struct ExprNode;
using Expr = std::variant<double, std::string, std::shared_ptr<ExprNode>>;
struct ExprNode {
char op; // '+', '-', '*', '/'
Expr left, right;
};
double eval(const Expr& e, const std::unordered_map<std::string, double>& vars) {
return std::visit([&](auto&& val) -> double {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, double>) {
return val;
} else if constexpr (std::is_same_v<T, std::string>) {
auto it = vars.find(val);
if (it == vars.end()) throw std::runtime_error("unknown var");
return it->second;
} else { // ExprNode
double l = eval(val->left, vars);
double r = eval(val->right, vars);
switch (val->op) {
case '+': return l + r;
case '-': return l - r;
case '*': return l * r;
case '/': return l / r;
default: throw std::runtime_error("bad op");
}
}
}, e);
}
int main() {
// Build (x + 3) * 2
auto tree = std::make_shared <ExprNode>();
tree->op = '*';
tree->left = std::make_shared <ExprNode>();
tree->left->op = '+';
tree->left->left = std::string("x");
tree->left->right = 3.0;
tree->right = 2.0;
std::unordered_map<std::string, double> vars = {{"x", 5}};
std::cout << eval(tree, vars) << '\n'; // 输出 16
}
6. 小结
std::variant让我们在单一对象中安全地存储多种类型,并通过std::visit或std::get_if访问它们。- 相比传统多态,
variant更加轻量、编译期安全,且更易于维护和扩展。 - 适用于值类型多态、配置、事件、表达式等多种场景。
通过在项目中引入 std::variant,你可以让代码更简洁、可维护且类型安全。祝你编码愉快!