精读C++20设计模式——行为型设计模式:解释器模式

前言

笔者的这个更多是整理出来的内容,我没用过解释器模式,或者说,即使我真设计过一点DSL,因为犯不着那么麻烦,我也就没有采用如此刻板的解释器模式

​ 如果你跳起来说——嘿这个我熟悉啊,我天天写的Python就是解释器语言,我说你跳。。。对了。还真是!我们的Python解释器还真就是解释器模式的产物。不过,我们往往真不用去写一个解释器语言,我们更多的是想在涉及到需要做DSL的时候,我们才会利用这个设计模式解决问题。

​ 解释器模式(Interpreter Pattern)在行为型设计模式家族中有一种独特的位置:它把语言的语法规则和执行逻辑以类与对象的形式编码到程序中,使得我们可以把某种小型语言(DSL)嵌入到应用里,并对输入文本做解析与求值。解释器模式并不一定意味着要实现完整的编译器,它更适合表达规则明确、语法相对简单的场景,例如配置表达式、过滤规则、搜索查询语法或简单计算器。

解释器模式是什么

解释器模式的核心是把“语法”的每一种构成元素(终结符与非终结符)对应到程序里的类,每个类负责“解释(interpret)”或“求值(evaluate)”自己所代表的语法片段。调用方不需要知道具体的语法细节,只需把文本交给解释器,解释器把文本转换成抽象语法树(AST)或者链式对象,然后执行解释动作,得出结果。换句话说,解释器把一门小型语言的语法与语义“面向对象化”——语法规则是类的组合,语义是类的方法实现。


基本样例:解析整数

我们先从最简单的场景开始:输入是一个十进制整数的字符串,我们想把它解释成一个整数值。这个例子既可以用标准库的 std::stoi/std::from_chars,也可以用解释器模式的“类化”实现来表达“终结符”的含义。

下面代码给出一个很轻量的解释器实现:Expression 作为抽象基类,Number 表示终结符(数字文本)。interpret 方法把上下文(这里是字符串)转换成数值结果。

// int_interpreter.cpp
#include <iostream>
  #include <memory>
    #include <string>
      #include <charconv>
        struct Context {
        std::string input;
        Context(std::string s): input(std::move(s)) {}
        };
        struct Expression {
        virtual ~Expression() = default;
        virtual long long interpret(const Context& ctx) const = 0;
        };
        struct Number : Expression {
        size_t pos{0};
        Number(size_t p): pos(p) {}
        long long interpret(const Context& ctx) const override {
        const char* str = ctx.input.c_str() + pos;
        long long val = 0;
        auto [ptr, ec] = std::from_chars(str, str + ctx.input.size() - pos, val);
        if (ec != std::errc()) {
        throw std::runtime_error("invalid number");
        }
        return val;
        }
        };
        int main() {
        Context ctx("12345");
        std::unique_ptr<Expression> expr = std::make_unique<Number>(0);
          std::cout << expr->interpret(ctx) << "\n"; // 12345
            }

​ 最初我们只需要把数字解析为值,所以 Number 直接从文本解析并返回数值。随着需求增加,我们把 Context 明确为解释器上下文(保存输入、当前位置、错误信息等),并把 Expression 作为统一接口,从而允许更多表达式类(例如二元运算)插入到体系当中。

样例进阶:数值表达式求值(支持 + - * / 和括号)

接下来我们实现一个更完整的解释器:把文本表达式解析为 AST,并对 AST 求值。我们使用经典的**递归下降解析(recursive descent parsing)**来把输入字符串转换成节点对象。节点类型包括 NumberNodeBinaryOpNode(加、减、乘、除)。每个节点实现 evaluate() 返回数值。

示例代码(较完整):

// expr_interpreter.cpp
#include <iostream>
  #include <memory>
    #include <string>
      #include <cctype>
        #include <stdexcept>
          struct Node {
          virtual ~Node() = default;
          virtual long long evaluate() const = 0;
          };
          struct NumberNode : Node {
          long long value;
          NumberNode(long long v): value(v) {}
          long long evaluate() const override { return value; }
          };
          struct BinaryNode : Node {
          char op;
          std::unique_ptr<Node> left, right;
            BinaryNode(char o, std::unique_ptr<Node> l, std::unique_ptr<Node> r)
              : op(o), left(std::move(l)), right(std::move(r)) {}
              long long evaluate() const override {
              long long a = left->evaluate();
              long long b = right->evaluate();
              switch (op) {
              case '+': return a + b;
              case '-': return a - b;
              case '*': return a * b;
              case '/':
              if (b == 0) throw std::runtime_error("division by zero");
              return a / b;
              }
              throw std::runtime_error("unknown operator");
              }
              };
              class Parser {
              public:
              Parser(std::string s): input(std::move(s)), pos(0) {}
              std::unique_ptr<Node> parse() {
                auto node = parseExpression();
                skipSpaces();
                if (pos != input.size()) throw std::runtime_error("unexpected input");
                return node;
                }
                private:
                std::string input;
                size_t pos;
                void skipSpaces() {
                while (pos < input.size() && std::isspace((unsigned char)input[pos])) ++pos;
                }
                std::unique_ptr<Node> parseNumber() {
                  skipSpaces();
                  size_t start = pos;
                  bool neg = false;
                  if (pos < input.size() && input[pos] == '-') { neg = true; ++pos; }
                  if (pos >= input.size() || !std::isdigit((unsigned char)input[pos]))
                  throw std::runtime_error("expected number");
                  long long val = 0;
                  while (pos < input.size() && std::isdigit((unsigned char)input[pos])) {
                  val = val * 10 + (input[pos] - '0');
                  ++pos;
                  }
                  return std::make_unique<NumberNode>(neg ? -val : val);
                    }
                    std::unique_ptr<Node> parseFactor() {
                      skipSpaces();
                      if (pos < input.size() && input[pos] == '(') {
                      ++pos; // consume '('
                      auto node = parseExpression();
                      skipSpaces();
                      if (pos >= input.size() || input[pos] != ')') throw std::runtime_error("missing )");
                      ++pos; // consume ')'
                      return node;
                      }
                      return parseNumber();
                      }
                      std::unique_ptr<Node> parseTerm() {
                        auto node = parseFactor();
                        while (true) {
                        skipSpaces();
                        if (pos < input.size() && (input[pos] == '*' || input[pos] == '/')) {
                        char op = input[pos++];
                        auto rhs = parseFactor();
                        node = std::make_unique<BinaryNode>(op, std::move(node), std::move(rhs));
                          } else break;
                          }
                          return node;
                          }
                          std::unique_ptr<Node> parseExpression() {
                            auto node = parseTerm();
                            while (true) {
                            skipSpaces();
                            if (pos < input.size() && (input[pos] == '+' || input[pos] == '-')) {
                            char op = input[pos++];
                            auto rhs = parseTerm();
                            node = std::make_unique<BinaryNode>(op, std::move(node), std::move(rhs));
                              } else break;
                              }
                              return node;
                              }
                              };
                              int main() {
                              std::vector<std::string> tests = {
                                "1+2*3",
                                "(1+2)*3",
                                "10 - 4 / 2",
                                "(2+3)*(4-1)"
                                };
                                for (auto &t : tests) {
                                try {
                                Parser p(t);
                                auto ast = p.parse();
                                std::cout << t << " = " << ast->evaluate() << "\n";
                                  } catch (const std::exception &e) {
                                  std::cerr << "parse error: " << e.what() << "\n";
                                  }
                                  }
                                  }

这套代码做了以下事情:先把字符串解析成 AST(NumberNodeBinaryNode),再通过 evaluate() 做递归求值。解析器遵循优先级规则:先解析 factor(数字或括号),再处理乘除(term),最后处理加减(expression)。


解释器模式的常见变种与扩展

解释器模式在工程实践中并非一刀切的模板,它有多种变体,常见的有以下几类:

一种比较直接的变体是AST + 解释器的组合:先把文本解析成 AST,后续可以对 AST 做多种操作(求值、优化、序列化、转成字节码等)。这种做法把语法分析与语义执行分离,有利于后续扩展。

另一个变体是直接解释(single-pass/interpreter-on-the-fly),即解析器在解析过程中即时执行,不生成持久 AST。它的优点在于少内存开销与实现简洁,但不利于做多次评估或优化。

词法/语法分层(lexer + parser) 是更工程化的做法:把输入先分解成 token,再用解析器处理 token。这在语法变复杂时非常必要,便于错误定位与扩展。

在并发/性能场景下,解释器还会有JIT 编译/字节码 风格的演化:把语句编译成中间字节码或直接机器指令,然后多次执行以获得更高性能。这已经超出“轻量解释器”的范畴,但在需要重复求值的 DSL 中非常有意义。

组合模式下,解释器也可以和其他行为型模式结合:例如用访问者(Visitor) 在 AST 上做不同的操作(打印、优化、评估),或者用策略(Strategy) 注入不同的语义评估策略(例如浮点或整数语义)。


总结

解释器模式解决了什么问题?

解释器模式要解决的问题是:当应用需要解析并执行某种文本化规则或小型语言时,如何把语言的语法和执行语义以模块化、面向对象的方式表达出来,使得语法扩展、规则演进和复用变得更简单。

如何解决?

它通过把语法单位映射为类(终结符和非终结符),每个类实现 interpret/evaluate 接口,从而把解析与执行流程以组合对象的方式组织起来。调用方只与解释器接口交互,而不关心内部的具体语法类。


各种变种的优劣对比(便于在工程中选型)
方案优点缺点适用场景
直接 AST + 解释器清晰、可调试、方便复用和多次评估;容易加入优化/遍历(Visitor)初始实现开销较大,需要内存保存 AST需要多次执行同一脚本或要做优化场景
直接解释(单遍即时执行)实现简单、内存占用低不利于调试、不能复用 AST、难以做优化简单脚本、一次性命令解析
lexer + parser(分层)结构清晰、容错好、扩展性强实现复杂度提高,需要写词法器语法复杂或需良好错误提示时
字节码 / JIT高性能、适合热路径多次执行实现复杂,调试难高频重复执行、性能敏感场景
结合访问者/策略等模式职责分离,便于增加语义操作设计复杂度上升需要多种对 AST 的不同操作(打印、类型检查、求值)
posted on 2025-10-04 16:57  lxjshuju  阅读(3)  评论(0)    收藏  举报