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

news/2025/10/4 17:03:24/文章来源:https://www.cnblogs.com/lxjshuju/p/19125739

精读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 的不同操作(打印、类型检查、求值)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/927364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js疑惑

textBox.addEventListener("keydown", function (event) { console.log(`You pressed "${event.key}".`);});这个函数接收的"keydown",到底是什么意思我还是没看懂为什么会这样写看着也…

使用 Git Submodule 管理微服务项目:从繁琐到高效 - 指南

使用 Git Submodule 管理微服务项目:从繁琐到高效 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consola…

如何识别网页用什么网站做的女装关键词排名

一个网站&#xff0c;其实说白了就是某几个特定功能的组合&#xff0c;而更换用户头像就在这些功能之中。今天就来做个测试&#xff0c;针对不同的用户&#xff0c;实现头像上传功能。先给大家展示下成品效果图&#xff1a;思路针对不同的用户上传头像&#xff0c;我们要为每一…

邯郸专业做网站多少钱做印刷网站公司哪家好

一、接口自动化测试中&#xff0c;会用到测试账号&#xff0c;如何合理运用账号&#xff1f; 账号一般用于接口登录、接口用例传参、操作sql等&#xff0c;目前账号是写到yaml配置文件里&#xff0c;如果1个账户使用会出现资源冲突&#xff0c;可以配置多个账号使用&#xff0…

佛山专业建设网站平台兼职python做网站

STM32定时器定时及其应用 定时器概述☆定时器相关配置CubeMX工程配置及程序实现固件库程序设计及实现 定时器概述 1. 工作原理 使用精准的时基&#xff0c;通过硬件的方式&#xff0c;实现定时功能。定时器核心就是计数器 2. 定时器分类   基本定时器&#xff08;TIM6~TIM7…

深入解析:单元测试学习+AI辅助单测

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

20251004国庆模拟4

对于 20251004 CSP-S 模拟的总结Part 1 题目 点击快速下载 有两道是洛谷的: T2: P5979 [PA2014] Druzyny T3: P2371 [国家集训队] 墨墨的等式 ⚠警告: P5979 和本场的 T3 并不完全一样。 Part 2 考试重要时间线 8:00…

珂朵莉树 ODT

能干什么/局限性 高效处理区间平推(区间赋值)的问题。 在随机数据下飞快。 如果没有区间平推,或者区间平推的操作数量可以被卡得很少甚至没有,就不适用。 前置知识set没了。 建点 每个点要维护一个区间,以及这个区…

2025多校CSP模拟赛2

2025多校CSP模拟赛2 狂写大树套树通过 \(T3\) 的救赎感。 T1 查询 第一眼感觉不好做。 首先直接找绝对没前途,考虑二分 \(v\)。 问题变成了统计 \(a_j+b_j\times c_i\le v\) 的数量,变换一下变成: \[c_i\le \frac{v…

网站查询访问界面设计模式读后感

如今人们对于住宅需求早已今非昔比&#xff0c;不但需要足够大的空间&#xff0c;而且对于住宅所处位置是否交通便利&#xff0c;环境如何&#xff0c;光照情况都有要求&#xff0c;但是最关注的问题还是住宅的安全问题。如今的社会科技发达&#xff0c;不法分子的手段也层出不…

io多路复用:reactor模型的封装及与上层简单业务的搭建(webserver)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

详细介绍:深入了解linux网络—— 基于UDP实现翻译和聊天功能

详细介绍:深入了解linux网络—— 基于UDP实现翻译和聊天功能pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Cons…

详细介绍:vLLM - GPUModelRunner

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Rewind: Codeforces Round 1055 (Div.1+Div.2)

E. Monotone Subsequence \(\text{time limit: 2000 ms}\\\text{memory limit: 1024 MB}\) 这是一道交互题 题意: 由 \(\text{Thm. Erdős–Szekeres}\) ,我们知道对任意长为 \(n^2+1\) 的排列必有一个长为 \(n+1\) …

10.4模拟赛总结

2025-2026 赛季 OIFHA 第三十四场 NOIP 模拟赛总结 一休尼(forever) 原题:CF5E Bindian Signalizing 长度为 \(n\) 的整数序列 \(a\) 。求整数对 \((i,j)\),\(i,j\in [1,n]\) 的个数,满足 \((i,j)\) 之间存在至少…

做网站动图的软件游戏开发软件有哪些

一、简介 java8新添加了一个特性&#xff1a;流Stream。Stream让开发者能够以一种声明的方式处理数据源&#xff08;集合、数组等&#xff09;&#xff0c;它专注于对数据源进行各种高效的聚合操作&#xff08;aggregate operation&#xff09;和大批量数据操作 (bulk data op…

重庆网站设计建设备案网站服务内容

探索AI图像安全&#xff0c;助力可信AI发展 0. 前言1. 人工智能发展与安全挑战1.1 人工智能及其发展1.2 人工智能安全挑战 2. WAIC 2023 多模态基础大模型的可信 AI2.1 WAIC 2023 专题论坛2.2 走进合合信息 3. AI 图像安全3.1 图像篡改检测3.2 生成式图像鉴别3.3 OCR 对抗攻击技…

01.linux基础

01.linux基础 1.你平时在公司主要做什么?2.你们原来公司的网站架构是怎么样的?3.你对哪一块比较熟练或者精通?4.介绍一下负载均衡?5.lvs 内部原理?6.nginx lvs haproxy 三个有什么区别?7.lvs 主要3种工作模式原…

详细介绍:Kubernetes实战:MariaDB误删恢复与数据持久化

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …