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

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

前言

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

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

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

解释器模式是什么

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


基本样例:解析整数

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

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

cpp 复制代码
// 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() 返回数值。

示例代码(较完整):

cpp 复制代码
// 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 的不同操作(打印、类型检查、求值)
相关推荐
zhangxuyu11183 小时前
flex布局学习记录
前端·css·学习
郭源潮13 小时前
《Muduo网络库:实现Channel通道以及Poller抽象基类》
服务器·c++·网络库
半桔5 小时前
【网络编程】深入 HTTP:从报文交互到服务构建,洞悉核心机制
linux·网络·c++·网络协议·http·交互
hqwest5 小时前
QT肝8天07--连接数据库
开发语言·数据库·c++·qt·sqlite·上位机·qt开发
WaWaJie_Ngen5 小时前
LevOJ P2080 炼金铺 II [矩阵解法]
c++·线性代数·算法·矩阵
三次拒绝王俊凯5 小时前
在ideal中访问页面时出现 HTTP 404 - Not Found
java·学习·tomcat
姝孟5 小时前
笔记(C++篇)—— Day 12(类的默认成员函数)
c++·笔记·学习
tpoog6 小时前
[C++项目组件]Elasticsearch简单介绍
开发语言·c++·elasticsearch
ideaout技术团队12 小时前
leetcode学习笔记2:多数元素(摩尔投票算法)
学习·算法·leetcode