在现代 C++(C++17 及以后)中,std::variant 提供了一种轻量级、类型安全的方式来实现多态。与传统的继承和虚函数相比,std::variant 不需要虚表开销,也不需要运行时的类型识别。本文将通过一个完整的示例来演示如何使用 std::variant 构建一个简易的表达式树,并通过访问器实现类型安全的求值与打印。
1. 需求与设计
假设我们需要表示四种基本表达式:
- 整数常量
IntVal - 浮点常量
FloatVal - 二元加法
AddExpr - 二元乘法
MulExpr
传统实现可能会用一个基类 Expr,并派生出四个子类。但这样做会导致:
- 虚函数表占用空间
- 需要手动
dynamic_cast或typeid进行类型检查 - 难以保证所有类型都被处理(遗漏某种类型时编译器不会警告)
std::variant 可以让我们把这些类型放进一个容器,然后通过 std::visit 或 std::holds_alternative 进行安全访问。我们只需保证在访问时使用所有可能的类型,编译器即可检查完整性。
2. 代码实现
#include <iostream>
#include <variant>
#include <memory>
#include <vector>
#include <cmath>
// ① 定义基础类型
struct IntVal { int value; };
struct FloatVal { double value; };
// ② 前向声明表达式类
struct AddExpr;
struct MulExpr;
// ③ 通过 std::variant 封装所有可能的表达式
using Expr = std::variant<
IntVal,
FloatVal,
std::shared_ptr <AddExpr>,
std::shared_ptr <MulExpr>
>;
// ④ 定义 AddExpr 与 MulExpr,内部存储子表达式
struct AddExpr {
Expr left;
Expr right;
};
struct MulExpr {
Expr left;
Expr right;
};
// ⑤ 访问器:求值
double eval(const Expr& e) {
struct EvalVisitor {
double operator()(const IntVal& iv) const { return static_cast <double>(iv.value); }
double operator()(const FloatVal& fv) const { return fv.value; }
double operator()(const std::shared_ptr <AddExpr>& a) const {
return eval(a->left) + eval(a->right);
}
double operator()(const std::shared_ptr <MulExpr>& m) const {
return eval(m->left) * eval(m->right);
}
};
return std::visit(EvalVisitor{}, e);
}
// ⑥ 访问器:打印
void print(const Expr& e) {
struct PrintVisitor {
void operator()(const IntVal& iv) const { std::cout << iv.value; }
void operator()(const FloatVal& fv) const { std::cout << fv.value; }
void operator()(const std::shared_ptr <AddExpr>& a) const {
std::cout << "(";
print(a->left);
std::cout << " + ";
print(a->right);
std::cout << ")";
}
void operator()(const std::shared_ptr <MulExpr>& m) const {
std::cout << "(";
print(m->left);
std::cout << " * ";
print(m->right);
std::cout << ")";
}
};
std::visit(PrintVisitor{}, e);
}
// ⑦ 工厂函数,便于构建表达式
inline Expr make_int(int v) { return IntVal{v}; }
inline Expr make_float(double v) { return FloatVal{v}; }
inline Expr make_add(const Expr& l, const Expr& r) {
return std::make_shared <AddExpr>(AddExpr{l, r});
}
inline Expr make_mul(const Expr& l, const Expr& r) {
return std::make_shared <MulExpr>(MulExpr{l, r});
}
// ⑧ 主程序示例
int main() {
// 表达式:(3 + 4.5) * (2 + 1)
Expr expr = make_mul(
make_add(make_int(3), make_float(4.5)),
make_add(make_int(2), make_int(1))
);
std::cout << "表达式: ";
print(expr);
std::cout << std::endl;
std::cout << "结果: " << eval(expr) << std::endl;
return 0;
}
代码说明
-
类型定义
IntVal与FloatVal分别封装整数和浮点常量。AddExpr与MulExpr存储左、右子表达式,使用std::shared_ptr防止递归结构导致大小不定。
-
Expr统一别名using Expr = std::variant<...>把所有可能的表达式类型包装进一个变体。- 这样
Expr既是一个值,又可以在运行时携带任意一个子类型。
-
访问器
eval与print使用std::visit与对应的访客对象实现多态行为。- 访客的每个
operator()对应Expr里的一个成员,保证了所有情况都被处理。
-
工厂函数
make_int、make_float、make_add、make_mul用来构造表达式,隐藏std::variant与std::shared_ptr的细节。
-
主程序
- 构造一个包含整数、浮点数以及二元运算的混合表达式。
- 通过
print打印表达式树,eval计算结果。
3. 与传统继承比较
| 维度 | 传统继承 + 虚函数 | std::variant 方案 |
|---|---|---|
| 内存占用 | 虚表指针(每个对象) | 变体大小固定,std::shared_ptr 内部指针 |
| 运行时开销 | 虚函数调用 + 动态分派 | std::visit 通过模板展开 |
| 类型安全 | 需要 dynamic_cast 或 RTTI |
编译器保证访问完整性 |
| 可维护性 | 需要在基类添加新成员、更新子类 | 只需在 std::variant 列表中添加新类型 |
4. 进一步扩展
- 三元运算符、函数调用 等都可以通过添加新的结构体类型,并更新访问器实现。
- 如果不想使用
std::shared_ptr,可以改用std::unique_ptr或直接存放对象,前提是子表达式大小已知。 - 对于更大规模的表达式树,
std::variant与std::visit的递归调用会产生一定的函数调用开销;此时可考虑使用手写访客模式或std::variant的apply_visitor。
5. 小结
std::variant 为 C++ 程序员提供了一种轻量级、类型安全的多态工具。与传统继承相比,它消除了虚表开销、消除了 RTTI 的使用,并让编译器在访问时提供完整性检查。通过本文的表达式树示例,你可以快速上手并在自己的项目中尝试这一模式。祝你编码愉快!