Interpreter模式不适用于实现完整语言解释器,仅适合固定、极小、结构已知的表达式(如数学计算、SQL WHERE片段、配置条件规则);硬套复杂语法会导致节点类爆炸、dynamic_cast泛滥、维护成本剧增。

解释器模式在 C++ 里真不适合写“语言解释器”
直接说结论:Interpreter 模式是为“固定、极小、结构已知”的表达式设计的,比如计算器里的 1 + 2 * 3 或正则子模式匹配。它不是用来实现一门语言(哪怕只有 if、print、变量)的——语法会迅速爆炸,维护成本远高于手写递归下降解析器。
常见错误现象:用 Interpreter 模式硬套 while 嵌套、作用域、函数调用,结果 AST 节点类膨胀到 20+ 个,interpret() 方法里全是 dynamic_cast 和空指针检查,一加新语法就得改所有节点。
- 适用场景仅限:数学表达式、SQL WHERE 子句片段、配置文件中的条件规则(如
status == "done" && retry ) - 不适用场景:带变量绑定、控制流、用户定义函数的语言
- 性能影响:每次执行都遍历整棵树,无缓存,无字节码,无法做常量折叠或死代码消除
真要写简单语言解释器,从递归下降 + AST 开始
比 Interpreter 模式靠谱得多:手动写 parseExpression()、parseStatement(),生成明确的 ASTNode 子类树,再统一 evaluate()。控制力强,调试直观,加新语法只动 parser 和 evaluator 两处。
示例关键骨架:
立即学习“C++免费学习笔记(深入)”;
struct ASTNode { virtual ~ASTNode() = default; };
struct BinaryOp : ASTNode { std::string op; std::unique_ptr<ASTNode> left, right; };
struct NumberLiteral : ASTNode { double value; };
<p>std::unique_ptr<ASTNode> parseExpression() {
auto left = parsePrimary();
while (match(<code>"+"</code>) || match(<code>"-"</code>)) {
auto op = previous().lexeme;
auto right = parsePrimary();
left = std::make_unique<BinaryOp>(BinaryOp{op, std::move(left), std::move(right)});
}
return left;
}</p>- 不要试图抽象出通用
Parser基类——不同语法层级语义差异大,强行泛化只会增加间接层 - 词法分析建议用手工状态机(几页代码),别过早引入
flex或正则库——简单语言 token 种类少,正则反而难 debug - AST 节点用
std::variant也行,但虚函数 + 继承对初学者更直观,且避免std::visit的模板推导陷阱
变量作用域和求值顺序最容易崩掉的地方
几乎所有新手写的“玩具解释器”都在这里翻车:全局变量表直接用 std::map<std::string, Value>,没嵌套作用域;if 分支里声明的变量泄漏到外层;函数参数绑定时机错乱。
必须立刻建立作用域链模型:
struct Environment {
std::shared_ptr<Environment> enclosing;
std::unordered_map<std::string, Value> values;
<pre class='brush:php;toolbar:false;'>void define(const std::string& name, const Value& value) { values[name] = value; }
Value get(const std::string& name) {
auto it = values.find(name);
if (it != values.end()) return it->second;
if (enclosing) return enclosing->get(name);
throw RuntimeError("Undefined variable '" + name + "'");
}};
-
enclosing字段不能漏——if、while、函数体都得新建Environment并指向当前环境 - 函数调用时,参数绑定必须在新环境里完成,不能修改调用方环境
- 别用
std::map存变量——查找慢,且std::stringkey 构造开销大;std::unordered_map更实际
没有 GC 的 C++ 解释器,Value 类型设计是隐形地雷
你很快会遇到:字符串怎么存?对象怎么引用?循环引用怎么破?C++ 里不处理好,就是野指针、重复析构、内存泄漏三连。
推荐方案:用 std::shared_ptr 包裹堆分配的对象,Value 用 std::variant 统一持有:
using ObjectPtr = std::shared_ptr<struct Object>;
struct Value {
std::variant<double, bool, std::string, ObjectPtr, std::monostate> data;
};
- 原始类型(
double、bool)直接存,别包装成对象——避免无谓的堆分配 -
std::string按值存没问题,C++11 后有短字符串优化(SSO),小字符串不堆分配 - 千万别用裸指针或
std::unique_ptr做对象引用——函数返回对象、变量赋值都会触发所有权转移,逻辑瞬间失控
最常被忽略的一点:解释器的 Value 必须能区分 “未定义” 和 “空值”。很多实现用 std::monostate 表示未初始化,但一旦忘了初始化就触发 std::bad_variant_access——这个异常在 eval 链深处极难定位。











