C++面向对象:如何在表达式解析中避免类型切换?
解决方案:两种替代动态类型转换的令牌设计方案
针对你在表达式解析器令牌化阶段遇到的类结构设计问题,这里提供两种成熟的替代方案,避免使用dynamic_cast带来的代码异味:
方案一:访问者模式(Visitor Pattern)
访问者模式是处理多态类型的经典方案,它将令牌的操作逻辑与令牌本身分离,既保持了多态性,又避免了显式类型判断。
实现步骤
- 定义访问者接口,包含针对每种令牌类型的处理方法
- 令牌基类添加纯虚的
accept方法,用于接受访问者 - 每个令牌子类实现
accept方法,调用访问者对应的处理方法 - 实现具体的访问者类,封装不同的操作逻辑(比如令牌打印、表达式求值)
代码示例
#include <memory> #include <vector> #include <iostream> // 前置声明访问者基类 class TokenVisitor; // 令牌基类 class Token { public: virtual ~Token() = default; virtual void accept(TokenVisitor& visitor) = 0; }; // 访问者基类 class TokenVisitor { public: virtual ~TokenVisitor() = default; virtual void visit(NumberToken& token) = 0; virtual void visit(OperatorToken& token) = 0; virtual void visit(BracketToken& token) = 0; }; // 运算符类型枚举 enum class OperatorType { Addition, Subtraction, Multiplication, Division }; // 括号类型枚举 enum class BracketType { Open, Close }; // 数值令牌 class NumberToken : public Token { public: int value; NumberToken(int val) : value(val) {} void accept(TokenVisitor& visitor) override { visitor.visit(*this); } }; // 运算符令牌 class OperatorToken : public Token { public: OperatorType type; OperatorToken(OperatorType t) : type(t) {} void accept(TokenVisitor& visitor) override { visitor.visit(*this); } }; // 括号令牌 class BracketToken : public Token { public: BracketType type; BracketToken(BracketType t) : type(t) {} void accept(TokenVisitor& visitor) override { visitor.visit(*this); } }; // 示例:打印令牌的访问者 class TokenPrinter : public TokenVisitor { public: void visit(NumberToken& token) override { std::cout << "数值令牌:" << token.value << std::endl; } void visit(OperatorToken& token) override { const char* opStr = nullptr; switch(token.type) { case OperatorType::Addition: opStr = "+"; break; case OperatorType::Subtraction: opStr = "-"; break; case OperatorType::Multiplication: opStr = "*"; break; case OperatorType::Division: opStr = "/"; break; } std::cout << "运算符令牌:" << opStr << std::endl; } void visit(BracketToken& token) override { std::cout << "括号令牌:" << (token.type == BracketType::Open ? "(" : ")") << std::endl; } }; // 使用示例 int main() { std::vector<std::shared_ptr<Token>> tokens = { std::make_shared<NumberToken>(100), std::make_shared<OperatorToken>(OperatorType::Addition), std::make_shared<BracketToken>(BracketType::Open) }; TokenPrinter printer; for (auto& token : tokens) { token->accept(printer); } return 0; }
优点
- 操作逻辑与令牌类解耦,新增操作只需添加新的访问者类,无需修改令牌代码
- 完全避免
dynamic_cast,代码类型安全且易维护 - 适合需要多种操作(如打印、求值、语法检查)的复杂场景
方案二:使用std::variant(C++17及以上)
如果你的项目支持C++17或更高版本,std::variant是更轻量的选择。它是类型安全的联合体,无需继承体系,直接存储不同类型的令牌,通过std::visit处理不同类型。
代码示例
#include <variant> #include <vector> #include <iostream> // 运算符类型枚举 enum class OperatorType { Addition, Subtraction, Multiplication, Division }; // 括号类型枚举 enum class BracketType { Open, Close }; // 各个令牌类型(无需继承基类) struct NumberToken { int value; }; struct OperatorToken { OperatorType type; }; struct BracketToken { BracketType type; }; // 定义令牌变体类型 using Token = std::variant<NumberToken, OperatorToken, BracketToken>; // 使用示例 int main() { std::vector<Token> tokens = { NumberToken{100}, OperatorToken{OperatorType::Addition}, BracketToken{BracketType::Open} }; // 用std::visit处理每个令牌 for (const auto& token : tokens) { std::visit([](const auto& t) { using T = std::decay_t<decltype(t)>; if constexpr (std::is_same_v<T, NumberToken>) { std::cout << "数值令牌:" << t.value << std::endl; } else if constexpr (std::is_same_v<T, OperatorToken>) { const char* opStr = nullptr; switch(t.type) { case OperatorType::Addition: opStr = "+"; break; case OperatorType::Subtraction: opStr = "-"; break; case OperatorType::Multiplication: opStr = "*"; break; case OperatorType::Division: opStr = "/"; break; } std::cout << "运算符令牌:" << opStr << std::endl; } else if constexpr (std::is_same_v<T, BracketToken>) { std::cout << "括号令牌:" << (t.type == BracketType::Open ? "(" : ")") << std::endl; } }, token); } return 0; }
优点
- 无需继承体系,代码更简洁轻便
- 编译期类型检查,运行时无额外开销
- 适合操作较少、追求代码简洁的场景
方案选择建议
- 如果你的解析器需要支持多种独立操作(如求值、格式化、语法验证),优先选择访问者模式,扩展性更好
- 如果项目使用C++17+且操作逻辑简单,std::variant是更高效简洁的选择
内容的提问来源于stack exchange,提问作者TNTzx
相关产品推荐
相关产品推荐

