news 2026/10/21 23:21:39

C++设计模式之解释器模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++设计模式之解释器模式

前言


解释器模式(Interpreter Pattern)解决的是一个很具体的问题:给定一种小型的、规则明确的语言,如何用面向对象的方式表示它的语法,并对其求值。经典例子是算术表达式、布尔表达式、查询条件、简单的命令脚本。它的做法是给文法中的每条规则定义一个类,语法树就是对象树,求值就是对树做一次递归遍历。


这个模式经常被误解成「写一个编程语言解释器」。实际上它适合的是文法很小、变化不频繁的场景。GoF 的书里明确写过:当文法复杂到一定程度,解释器模式会因为类的数量爆炸而变得难以维护,此时应该改用语法分析器生成器。把解释器模式当成「手写一个迷你语言的骨架」更准确,而不是「实现一门完整语言的方法」。


另一个常见的混淆是解释器模式与组合模式(Composite)。两者的结构确实很像:都有一致的抽象基类,都有叶子节点和容器节点。区别在意图:组合模式关心「部分—整体」的树形结构与统一操作,解释器模式关心「用一个类表示一条文法规则」。实际实现中,解释器模式的抽象语法树往往就是一棵组合结构。


本文先讲模式的角色划分,再讲文法和递归下降解析,然后给出一个完整的算术表达式解释器,最后讨论它的边界以及和 Visitor 模式的分工。代码按 C++17 编写,没有在本机编译或运行过。


一、模式的角色与结构


解释器模式有四个角色。




角色职责本例中的类型



AbstractExpression声明interpret接口Expression

TerminalExpression文法中的终结符,不再包含子表达式NumberExpr、VariableExpr

NonterminalExpression文法中的非终结符,持有子表达式AddExpr、SubExpr、MulExpr、DivExpr

Context保存解释过程中需要的全局信息Context



调用方负责构建语法树,然后对根节点调用interpret,把Context传进去。每个非终结符在实现里递归调用子节点的interpret,把结果组合起来。终结符直接返回自身的值,或者从Context里查。


这个结构的关键在于「求值逻辑分散在各个节点类里」。每个节点只关心自己那一条规则怎么算,不需要知道整棵树长什么样。新增一条文法规则,就是新增一个类;新增一种「对树做点什么」的操作,则要修改所有类。这个取舍在最后一节会展开。


二、文法、语法树与递归下降


在写类之前,先把文法写清楚。算术表达式的经典文法如下,expr、term、factor是非终结符,数字和标识符是终结符。


expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := number | identifier | '(' expr ')'

这个文法把优先级编码进了层次:expr处理加减,term处理乘除,factor处理括号和原子。因为每一层只引用下一层,不存在expr := expr '+' term这样的左递归,递归下降解析器可以直接按文法写成三个函数,每个函数消费一部分输入并返回一棵子树。


语法树与类结构的对应关系是直接的:2 * (x + y) - 4解析后,根是SubExpr,左子节点是MulExpr,它的右子节点是AddExpr,叶子分别是NumberExpr(2)、VariableExpr("x")、VariableExpr("y")、NumberExpr(4)。对根调用interpret,递归自然完成求值。


递归下降的写法有一个前提:文法不能有左递归,否则解析函数会无限调用自己。真实语言的文法通常需要先消除左递归或改用运算符优先级爬升(precedence climbing),本例的文法已经按优先级分层,不需要额外处理。


三、完整实现:算术表达式解释器


下面的代码包含上下文、节点类、解析器和入口函数,是一个可以独立编译的完整程序。为控制篇幅,只支持整数、四则运算、括号和变量。


// 适用于 C++17
#include <cctype>
#include <cstddef>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

class Context {
public:
void set(const std::string& name, int value)
{
vars_[name] = value;
}

int get(const std::string& name) const
{
auto it = vars_.find(name);
if (it == vars_.end()) {
throw std::runtime_error("undefined variable: " + name);
}
return it->second;
}

private:
std::map<std::string, int> vars_;
};

class Expression {
public:
virtual ~Expression() = default;
virtual int interpret(const Context& ctx) const = 0;
};

class NumberExpr : public Expression {
public:
explicit NumberExpr(int value) : value_(value) {}
int interpret(const Context&) const override { return value_; }

private:
int value_;
};

class VariableExpr : public Expression {
public:
explicit VariableExpr(std::string name) : name_(std::move(name)) {}
int interpret(const Context& ctx) const override { return ctx.get(name_); }

private:
std::string name_;
};

class BinaryExpr : public Expression {
public:
BinaryExpr(std::unique_ptr<Expression> lhs,
std::unique_ptr<Expression> rhs)
: lhs_(std::move(lhs)), rhs_(std::move(rhs)) {}

protected:
std::unique_ptr<Expression> lhs_;
std::unique_ptr<Expression> rhs_;
};

class AddExpr : public BinaryExpr {
public:
using BinaryExpr::BinaryExpr;
int interpret(const Context& ctx) const override
{
return lhs_->interpret(ctx) + rhs_->interpret(ctx);
}
};

class SubExpr : public BinaryExpr {
public:
using BinaryExpr::BinaryExpr;
int interpret(const Context& ctx) const override
{
return lhs_->interpret(ctx) - rhs_->interpret(ctx);
}
};

class MulExpr : public BinaryExpr {
public:
using BinaryExpr::BinaryExpr;
int interpret(const Context& ctx) const override
{
return lhs_->interpret(ctx) * rhs_->interpret(ctx);
}
};

class DivExpr : public BinaryExpr {
public:
using BinaryExpr::BinaryExpr;
int interpret(const Context& ctx) const override
{
int divisor = rhs_->interpret(ctx);
if (divisor == 0) {
throw std::domain_error("division by zero");
}
return lhs_->interpret(ctx) / divisor;
}
};

class Parser {
public:
explicit Parser(std::string text) : text_(std::move(text)) {}

std::unique_ptr<Expression> parse()
{
auto expr = parse_expr();
skip_space();
if (pos_ != text_.size()) {
throw std::runtime_error("unexpected trailing input");
}
return expr;
}

private:
std::unique_ptr<Expression> parse_expr()
{
auto lhs = parse_term();
for (;;) {
skip_space();
if (pos_ >= text_.size() ||
(text_[pos_] != '+' && text_[pos_] != '-')) {
break;
}
char op = text_[pos_++];
auto rhs = parse_term();
if (op == '+') {
lhs = std::make_unique<AddExpr>(std::move(lhs), std::move(rhs));
} else {
lhs = std::make_unique<SubExpr>(std::move(lhs), std::move(rhs));
}
}
return lhs;
}

std::unique_ptr<Expression> parse_term()
{
auto lhs = parse_factor();
for (;;) {
skip_space();
if (pos_ >= text_.size() ||
(text_[pos_] != '*' && text_[pos_] != '/')) {
break;
}
char op = text_[pos_++];
auto rhs = parse_factor();
if (op == '*') {
lhs = std::make_unique<MulExpr>(std::move(lhs), std::move(rhs));
} else {
lhs = std::make_unique<DivExpr>(std::move(lhs), std::move(rhs));
}
}
return lhs;
}

std::unique_ptr<Expression> parse_factor()
{
skip_space();
if (pos_ >= text_.size()) {
throw std::runtime_error("unexpected end of input");
}

char c = text_[pos_];
if (c == '(') {
++pos_;
auto inner = parse_expr();
skip_space();
if (pos_ >= text_.size() || text_[pos_] != ')') {
throw std::runtime_error("expected ')'");
}
++pos_;
return inner;
}

if (std::isdigit(static_cast<unsigned char>(c))) {
int value = 0;
while (pos_ < text_.size() &&
std::isdigit(static_cast<unsigned char>(text_[pos_]))) {
value = value * 10 + (text_[pos_] - '0');
++pos_;
}
return std::make_unique<NumberExpr>(value);
}

if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
std::size_t start = pos_;
while (pos_ < text_.size() &&
(std::isalnum(static_cast<unsigned char>(text_[pos_])) ||
text_[pos_] == '_')) {
++pos_;
}
return std::make_unique<VariableExpr>(
text_.substr(start, pos_ - start));
}

throw std::runtime_error(std::string("unexpected character: ") + c);
}

void skip_space()
{
while (pos_ < text_.size() &&
std::isspace(static_cast<unsigned char>(text_[pos_]))) {
++pos_;
}
}

std::string text_;
std::size_t pos_ = 0;
};

int main()
{
Context ctx;
ctx.set("x", 6);
ctx.set("y", 7);

try {
Parser parser("2 * (x + y) - 4");
auto expr = parser.parse();
std::cout << expr->interpret(ctx) << '\n';
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << '\n';
return 1;
}
return 0;
}

这段实现有几处值得注意的工程细节。


第一,抽象基类的析构函数写成了virtual ~Expression() = default;。语法树由std::unique_ptr<Expression>持有,删除时通过基类指针进行,析构函数不是虚函数就是 UB——这是解释器模式里最容易犯又最难察觉的错误,因为它在很多实现上「看起来正常」。


第二,节点一律用std::unique_ptr持有子节点,BinaryExpr的构造函数接管所有权。解析器构造新节点时用std::make_unique,中途抛出异常也不会泄漏。不要用裸new构建语法树。


第三,DivExpr先求出除数再检查零,除数为零时抛std::domain_error。整数除法本身在除数为零时是 UB,必须在调用之前拦住,不能依赖硬件异常。


第四,解析器在parse()里检查了pos_ == text_.size()。少了这一步,"1 + 2 abc"会被当成解析成功,因为parse_expr只要匹配到1 + 2就返回了,后面的字符被静默忽略。这是手写解析器最常见的漏洞。


第五,std::isdigit、std::isalpha、std::isspace的参数都做了static_cast<unsigned char>。这些函数要求参数是「能表示为unsigned char的值或EOF」,直接传负的char是 UB。C++ 里char是否有符号由实现决定,不能想当然。


四、适用边界与相关模式


解释器模式的优点是结构清晰、扩展文法方便:新增一种运算,就是新增一个继承Expression的类,其余代码不动。缺点是每个节点一个类,文法一大,类的数量就线性增长,而且求值逻辑散落在各处,想加一种新操作(比如「打印语法树」「做常量折叠」「生成目标代码」)就必须修改每一个节点类。这就是所谓的表达式问题(expression problem):很难同时做到「新增类型容易」和「新增操作容易」。


Visitor 模式解决的是另一条轴:把「操作」从节点类里抽出来,新增操作只需新增一个 visitor 类,但新增节点类型要修改所有 visitor。两者经常配合使用:语法树用解释器模式的结构,遍历和变换用 Visitor。


什么时候不该用解释器模式:



  • 文法规模大(比如完整的 SQL、JSON Schema、正则表达式)。手写类层次不现实,应该用解析器生成器,或者现成的解析库。

  • 文法经常变化。每次改文法都要改类层次和解析器,维护成本高。

  • 对性能极其敏感。虚函数调用加指针追逐的树遍历,通常不如一次编译成字节码再执行;但这属于定性判断,具体差异要测量。

  • 只需要一次性求值、不需要复用语法树。此时直接边解析边计算(比如调度场算法)更简单。


如果语法树不需要支持多种操作,C++17 的std::variant加std::visit也是一个紧凑的替代方案:用variant<Number, Binary, ...>表示节点,用visit做求值,避免了一堆小类和虚函数,代价是节点类型集合固定、编译期展开。选哪种取决于你更看重扩展性还是简洁性。


常见坑点



  1. ❌Expression的析构函数不写virtual,然后用unique_ptr<Expression>管理整棵树。


✅ 写virtual ~Expression() = default;;通过基类指针删除非虚析构对象是 UB。



  1. ❌ 解析完成后不检查是否还有剩余输入。


✅ 在入口处检查位置指针是否到达末尾,否则非法尾部会被静默忽略。



  1. ❌ 整数除法直接算,指望除数为零时能抛异常。


✅ 先判断除数是否为零并抛出std::domain_error;整数除零是 UB。



  1. ❌ 用裸new构造节点,解析中途抛异常导致整棵树泄漏。


✅ 用std::unique_ptr和std::make_unique,让所有权和异常安全由 RAII 保证。



  1. ❌ 变量未定义时返回 0,让错误悄悄传播。


✅ 抛出异常或返回std::optional,把「未定义」和「值为 0」区分开。



  1. ❌ 把char直接传给std::isdigit、std::isspace。


✅ 先转换:std::isdigit(static_cast<unsigned char>(c)),避免负值导致的 UB。



  1. ❌ 文法写成左递归(expr := expr '+' term),递归下降直接栈溢出。


✅ 改写成循环形式或分层文法,保证每个解析函数在递归前先消费输入。



  1. ❌ 把解释器模式和 Visitor 模式混为一谈,节点里既放求值又放十种遍历操作。


✅ 按「类型扩展」还是「操作扩展」的主方向选一个;两者结合时,把操作集中到 visitor 里。


总结




方面说明



核心思想一条文法规则一个类,语法树即对象树,求值即递归遍历

关键角色抽象表达式、终结符、非终结符、上下文

构建方式手写递归下降,文法按优先级分层,避免左递归

扩展方向新增文法规则容易,新增操作需改动所有节点

内存管理unique_ptr持有子树,基类析构必须为虚

适用边界小型、稳定的领域语言;大型文法改用解析器生成器



解释器模式真正提供的是一套「把语法变成类型」的思路:一旦文法被类型化,求值、检查、优化、打印都可以围绕这棵树展开。它的成本也来自同一处——类型数量随文法增长。先用它把最小可用的语义跑通,再根据「改文法多还是改操作多」决定是否引入 Visitor,通常比一开始就设计一套庞大的类层次更务实。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 23:20:08

纯电动车制动能量回收的ECE法规与I线协同设计

简介&#xff1a;本资源是一篇发表于《武汉理工大学学报&#xff08;交通科学与工程版&#xff09;》的学术研究论文&#xff0c;面向新能源汽车领域研究生、车辆工程专业研究人员及电控系统工程师&#xff0c;聚焦纯电动汽车制动能量回收策略的核心难点——如何在满足ECE-R13法…

作者头像 李华
网站建设 2026/10/21 23:18:01

SQL优化实战:从慢查询定位到索引失效排查,一次讲透

前阵子接手了一个快到期的线上改造任务&#xff0c;业务方报障说某个查询页面打开越来越慢&#xff0c;从点击到看到结果经常要等五六秒&#xff0c;有时候直接超时。一开始我以为是接口代码问题&#xff0c;结果翻了一遍发现逻辑很简单&#xff0c;就是一次单表查询加一次列表…

作者头像 李华
网站建设 2026/10/21 23:17:16

基于Python的网易云排行榜数据分析系统:从数据采集到可视化全流程

做数据分析这些年&#xff0c;我一直有个观点&#xff1a;管你模型调得再好、图表画得再漂亮&#xff0c;数据源头一旦不干净&#xff0c;后面全是白费。这套基于Python的网易云排行榜数据分析系统&#xff0c;就是围绕“数据从哪来、怎么存、怎么洗、怎么用”这条主线做的。榜…

作者头像 李华
网站建设 2026/10/21 23:15:46

Python五子棋实战:状态机设计与鲁棒性实现

简介&#xff1a;本资源是一份面向计算机专业本科生的Python毕业设计完整实践材料&#xff0c;聚焦五子棋游戏开发全流程&#xff0c;帮助学习者掌握GUI编程、游戏逻辑设计与软件工程规范。压缩包含1个253KB的Word文档&#xff08;.doc&#xff09;&#xff0c;完整涵盖摘要、需…

作者头像 李华
网站建设 2026/10/21 23:15:33

S7-1200与V20变频器MODBUS RTU通讯调试核心:控制字切换与物理层校准

简介&#xff1a;本资源是一份面向工业自动化工程师与PLC初学者的实操型技术文档&#xff0c;聚焦S7-1200 PLC通过外部端子控制V20变频器启停&#xff0c;并基于MODBUS RTU协议实现频率读写的核心方法。内容覆盖硬件接线规范&#xff08;CB1241模块与V20端子的屏蔽双绞线连接&a…

作者头像 李华
网站建设 2026/10/21 23:15:05

独立开发三年实战:需求验证、MVP边界与技术选型的避坑指南

如果把独立开发看作一场长跑&#xff0c;我恰好跑到了第三年。这三年里&#xff0c;我一个人走完了从需求、设计、开发到上线运营的整个闭环&#xff0c;经历过第一批用户带来的兴奋&#xff0c;也经历过连续好几个月没有增长、甚至想放弃的时刻。今天这篇内容不聊某个具体框架…

作者头像 李华