首页 > 编程笔记 > C++笔记 阅读:15

C++解释器模式及其实现(非常详细)

解释器模式(Interpreter Pattern)是一种用于定义语言的文法并建立一个解释器来解释该语言中的句子。

解释器模式主要应用于使用面向对象语言开发的编译器中,描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。

解释器模式的核心思想是识别文法、构建解释。它通过抽象语法树(AST)等手段来描述语言的构成,并将每个终结符表达式、非终结符表达式都映射到一个具体的实例。这样,系统的扩展性比较好,因为每种终结符表达式、非终结符表达式都会有一个具体的实例与之相对应。

解释器模式的应用非常广泛,例如正则表达式就是一种常见的解释器模式应用。正则表达式定义了一个文法,通过特定的规则来匹配字符串,并可以灵活地解释这些规则。此外,解释器模式还可以应用于其他需要解析和解释特定语言的场景,如 XML 解析、JSON 解析等。

传统解释器模式

传统解释器通常包括抽象表达式(Abstract Expression,简写为absExpression)、上下文(Context)和客户端(Client),如下图所示。


图 1 传统解释器模式UML简图

传统的解释器模式通常需要构建语法和抽象的语法树,这对于大多数开发者来讲工作量繁杂且容易出错。创建一个新的语法无论对于开发者还是使用者来讲都需要大量的研发和学习的投入。

而现在市场上存在着大量的普及的语言种类,包括编译型的语言和非编译型的语言,其中不少语言可以和 C/C++ 配合起来使用。

C++11元编程下的结构设计

在实际的工程应用中通常不会设计新的语法规则和构造 AST。这会造成大量的开发时间浪费,并不能从中获得足以支撑项目的回报,因此在绝大多数情况下,工程上使用现有的脚本语言作为自身项目的解释器。

在目前的市场上存在着大量的优秀脚本语言解释器,其中很多种提供了和 C/C++ 的交互接口,例如 JavaScript 的 V8、Python,特别是 Lua,为了在 C/C++ 中嵌入脚本而进行开发,另外还有 ChaiScript 项目,这是一个专为在 C++ 中嵌入脚本语言而开发的项目。

使用现有的脚本语言不但能够有效地降低用户的学习成本,而且能够有效地降低项目的开发成本及开发周期,因此在本书中定义了一个通用接口以图利用市场上现有的语言种类,特别是一些脚本语言,例如 Lua、JavaScript、Python 等,作为实际的语法模块和解释器实现具体可以在工程中使用的解释器模式模块。

在实际工程中使用时仅需要实现和具体语言的接口部分就能够轻松地实现解释模块,如下图所示:


图 2 C++11 模板解释器模式 UML 简图

C++解释器模式实现和解析

本节中的实现主要采用统一操作接口,将实际的解释执行部分交由外部实现,主要分为 3 部分,一是规范接口继承关系;二是接口定义;三是引出针对 C++ 使用的方法,实现了通过异步调用外部语言的功能,代码如下:
//designM/interpretor.hpp

namespace itprtor_private__
{
    struct interpretor_interface__{};
}

template< typename RET,typename...PARAMS >
struct itptorItfc:public interpretor_interface__
{
    virtual RET execString(const std::string&str,PARAMS...args) = 0;
    virtual RET execFile(const std::string&file,PARAMS...args) = 0;
};

使用 struct itptorItfc 定义接口在使用上过于简单,在实际工程中容易造成接口不足。下面定义 3 个宏用来弥补这个不足,通过 3 个宏配合可以方便地声明更加灵活的接口,代码如下:
//designM/interpretor.hpp

#define DECLARE_INTERPRETOR_ITFC(name)\
struct name:public interpretor_interface__

#define INTERPRETOR_ITFC(RET,name,...) virtual RET name(__VA_ARGS__) = 0;

#define END_DECLARE_INTERPRETOR_ITFC() };

实际解释器可以使用外部的脚本语言,例如 Lua、JavaScript 和 Python 等。如何调用这些不同的解释语言,甚至自定义自己的语言,可以针对 IMPL_TYPE 进行具体实现,代码如下:
#ifndef DESIGNM_INTERPRETOR_HPP
#define DESIGNM_INTERPRETOR_HPP

#include <memory>
#include <type_traits>

template <typename ITFC_TYPE, typename IMPL_TYPE>
class interpretor
{
public:
    using itfc_t = typename std::remove_pointer<typename std::decay<ITFC_TYPE>::type>::type;
    using impl_t = typename std::remove_pointer<typename std::decay<IMPL_TYPE>::type>::type;

    // 编译期断言:IMPL_TYPE 必须派生自 ITFC_TYPE
    static_assert(std::is_base_of<itfc_t, impl_t>::value, "IMPL_TYPE must inherit from ITFC_TYPE");

protected:
    // 实际的脚本引擎对象
    std::shared_ptr<impl_t> pt_impl_;

public:
    interpretor() = default;
    virtual ~interpretor() = default;

    // 工厂函数:创建解释器对象,并构造实际的解释器引擎
    template<typename... Params>
    static std::shared_ptr<interpretor<itfc_t, impl_t>>
    make_shared(Params&&... args)
    {
        auto ret = std::make_shared<interpretor<itfc_t, impl_t>>();
        // 构造实际的解释器引擎
        ret->pt_impl_ = std::make_shared<impl_t>( std::forward<Params>(args)...);
        return ret;
    }
};

#endif // DESIGNM_INTERPRETOR_HPP
函数 execStringAsync() 和函数 execFileAsync() 用来异步地执行脚本任务。考虑到 C++ 的执行效率通常高于外部的脚本解释器,模块没有提供同步的执行方法。execStringAsync() 函数用来执行一段字符串,execFileAsyn() 函数则用来执行给定的脚本文件。

执行结果通过 std::future 对象返回,通过回调函数方式包覆进行执行处理,可以方便地在执行前和执行后完成一些自定义的处理动作。两个执行方法都支持以可变参数的方式提交参数,增加了方法的通用性,代码如下:
//designM/interpretor.hpp

template< typename Func_t,typename...Args >
auto execStringAsync(const std::string&str, Func_t&&func, Args&&...args)
->std::future< typename std::result_of<Func_t(std::shared_ptr<impl_t >,   const std::string&,  Args...) >::type >
{

两个执行解释器的方法都使用元函数 std::result_of 推导返回值的类型,然后使用 std::packaged_task 封包任务函数,最后获取异步返回的 std::future 对象 ret。最好使用 std::thread 异步执行任务,返回结果则通过 std::future 对象获取,代码如下:
// designM/interpretor.hpp
    using rst_type = typename std::result_of< Func_t(std::shared_ptr<impl_t>, const std::string&, Args...)>::type;

    std::packaged_task<rst_type()>
    task(std::bind(std::move(func), pt_impl__, str, std::forward<Args>(args)...));
    auto ret = task.get_future();
    std::thread thd(std::move(task));
    thd.detach();
    return ret;
}
template< typename Func_t, typename... Args >
auto execFileAsync(const std::string& file, Func_t && func, Args&&... args)// 参数表结束
-> std::future< typename std::result_of< Func_t(std::shared_ptr<impl_t>, const std::string&, Args...)>::type >
{
    using rst_type = typename std::result_of< Func_t(const std::string&, std::shared_ptr<impl_t>, const std::string&, Args...)>::type;
    std::packaged_task<rst_type()> task(std::bind(std::move(func), pt_impl__, file,std::forward<Args>(args)...));
    auto ret = task.get_future();
    std::thread thd(std::move(task));
    thd.detach();
    return ret;
}
};

C++解释器模式应用示例

下面的示例使用 Lua 脚本语言解释器实现一个语法解释器。接口使用了模块预定义的接口,并进行特化和实现,头文件、相关设计模式头文件和准备工作的代码如下:
//luaInptor.cpp

#include<iostream >
#include<lua.hpp >
#include<lualib.h >
#include<lauxlib.h >

#include"designM/interpretor.hpp"
using namespace wheels;
using namespace dm;

特化接口模块定义别名 itfc_type,并从接口模块 itfc_type 继承实现 LuaInterpreter 和 Lua 解释器模块。在这个模块中针对 execString 实现具体功能,使用 Lua 引擎执行提供的脚本并返回计算结果,代码如下:
//luaInptor.cpp
using itfc_type = itptorItfc<int>;
// Lua解释器实现类
class LuaInterpreter : public itfc_type {
public:
    LuaInterpreter() {}
    virtual int execFile(const std::string& file) override { return 0; }
    virtual int execString(const std::string& str) override {
        auto* L = luaL_newstate();
        // 初始化Lua状态机
        // 输出脚本内容
        std::cout << str << std::endl;
        luaL_openlibs(L);
        // 加载标准库
        // luaL_loadstring加载并执行Lua脚本,lua_pcall执行脚本内容
        if (luaL_loadstring(L, str.c_str()) || lua_pcall(L, 0, 1, 0)) {
            std::cerr << "Failed to execute Lua script: " << lua_tostring(L, -1)
                      << std::endl;
            lua_close(L);
            // 清理Lua状态机
            return -1;
            // 返回错误码
        } else {
            // 执行成功,返回结果
            int result = lua_tonumber(L, -1);
            // 获取结果,并关闭Lua状态机
            lua_close(L);
            return result;
        }
    }
};

创建 Lua 执行器对象,用来使用 Lua 脚本作为解释器,函数 func 是实际的执行部分,在这个函数内部 execString() 方法调用 Lua 解释执行并将结果返回。参数 str 就是要执行的脚本内容,pimpl 是解释器实现对象的指针,代码如下:
//luaInptor.cpp
using luaExecutor = interpretor<itfc_type, LuaInterpreter>;

int func(std::shared_ptr<luaExecutor::impl_t> pimpl, const std::string& str)
{
    if (pimpl){
        return pimpl->execstring(str);
    }
    return -1;
}
int main()
    // 创建执行器指针,用于异步执行 Lua 脚本
    auto luaExecutorPtr = luaExecutor::make_shared();
    // 异步执行 Lua 脚本,并获取结果 future。这里要执行的脚本就是
    // return 2 + 2
    auto luaFuture = luaExecutorPtr->execstringAsync("return 2 + 2", func);
    // 因为对外执行方法是异步执行,所以这里要等候一段时间以让脚本完成
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // 获取结果并输出,等待异步执行完成
    std::cout << "Lua script result: " << luaFuture.get() << std::endl;
    return 0;
}
以上代码的执行结果如下,第 1 行是脚本内容,第 2 行是执行结果:

return 2 + 2
Lua script result:4

相关文章