boost框架

    技术2024-06-01  92

    对于C ++程序员来说,更复杂的任务之一是在合理的时间段内对解析器进行编码。 在为SQL或C ++等成熟的语言开发编译器时,使用GNU Flex / Bison或ANTLR解析器生成器通常很有意义。 但是对于使用更简单的Backus Naur Form(BNF)的语法而言,这些工具的陡峭学习曲线并不总是能够证明投资的合理性。 另一种选择是使用与标准Linux®发行版或Boost regex或tokenizer库捆绑在一起的regex库,但是这些库在使用更多涉及的语法时无法很好地扩展。

    本文介绍了Boost的高度可扩展的Spirit解析器框架。 该解析器生成器使用C ++编码的扩展Backus Naur形式(EBNF)规范工作,从而大大减少了开发时间。 有关进一步的阅读,请参阅非常详细的Spirit文档。

    安装精神

    您可以从Boost网站免费下载Spirit框架(请参阅参考资料部分)。 在开始使用Spirit进行开发之前,请注意以下几点:

    您必须在源代码中包含<spirit.hpp>标头。 该头文件大量使用模板元编程和函子。 本文中的所有代码均已使用g ++-3.4.4进行了编译。 确保您使用的是支持这些C ++功能的编译器。 Spirit框架的一部分在内部使用Boost的正则表达式库。 安装后,在已安装的代码库中检查regex.h标头。 确保Boost安装的根目录在编译器include搜索路径中。 Spirit是仅标头的库,因此在链接时不需要额外的库。 对于regex库而言并非如此。 要将regex源仅包括为标头, define BOOST_SPIRIT_NO_REGEX_LIB在代码中使用preprocessor指令define BOOST_SPIRIT_NO_REGEX_LIB 。

    您的第一个Spirit程序

    给定一个随机的单词列表,采用真正的C ++风格,您的第一个Spirit程序列出在列表中找到的唯一出现的Hello World (即输入流中连续出现的Hello和World单词)的数量。 参见清单1; 输出为2。

    清单1.代码列出单词Hello World在输入流中出现的次数
    #define BOOST_SPIRIT_NO_REGEX_LIB #include "regex.h" #include "spirit.hpp" #include "boost/spirit/actor.hpp" using namespace boost::spirit; const string input = "This Hello World program using Spirit counts the number of Hello World occurrences in the input"; int main () { int count = 0; parse (input.c_str(), *(str_p("Hello World") [ increment_a(count) ] | anychar_p) ); cout << count >> endl; return 0; }

    Spirit框架的强大之处在于它为许多基本类型(例如单个字符,数字和字符串)提供了内置的解析器。 通常使用这些内置的解析器对象创建更复杂的解析器。 在清单1中 , str_p和anychar_p从精神预定义解析器- str_p它与提供的字符串匹配(在这种情况下, 世界您好 ),并成功调用increment_a由1例行增量次数anychar_p是另一个预定义解析器,任何比赛字符。

    现在让我们来看一下parse函数,它可以说是Spirit框架中最重要的例程。 它接受输入流和语法,并在内部通过该语法运行流。 在这种情况下,输入流来自input.c_str() ,而str_p和anychar_p提供了语法的语义。 如果您熟悉解析,您会很快意识到parse函数的第二个参数等效于提供BNF。

    其他预定义的Spirit解析器

    考虑遵循此模式的字符串: <employee name: string> <employee id: int> <employee rating: float> 。 您需要基于从此字符串提取的数据来填充Employee数据结构。 这是一个典型的字符串: "Alex 8 9.2 Jim 91 5.6" 。

    Spirit具有用于字符( alpha_p ),整数( int_p )和实数( real_p )的预定义解析器。 基于此,可以肯定地说应该使用如下语法调用parse例程: parse(input.c_str(), alpha_p >> int_p >> real_p) 。 逻辑是, parse将首先在输入流中按顺序查找字符串,然后查找整数,最后查找实数。 这样行吗? 否。清单2显示了一个可以为您解析此数据的工作代码段。

    清单2.使用alpha_p,int_p和real_p预定义的解析器
    #define BOOST_SPIRIT_NO_REGEX_LIB #include "regex.h" #include "spirit.hpp" #include "boost/spirit/actor/assign_actor.hpp" using namespace std; using namespace boost::spirit; const string input = "Alex 8 9.2 Jim 91 5.6"; typedef struct { string name; int idcode; float rating; } Employee; int main () { string name; int idcode; float rating; int status = parse (input.c_str(), *((+alpha_p) [assign_a(name)] >> ' ' >> int_p[assign_a(idcode)] >> ' ' >> real_p[assign_a(rating)] >> !blank_p) ).full; cout << status << endl; return 0; }

    原始调用由于以下原因而失败:

    alpha_p解析单个字符。 要解析字符串,您必须使用+alpha_p (这类似于EBNF +运算符,表示一个或多个,但Spirit必须在之前和之后使用它)。 空格用于分隔字符串,整数和实数。 这种现象必须予以考虑。 您可以通过两种方式进行操作:使用' ' ; 或者,最好使用blank_p预定义解析器,该解析器同时考虑空格和制表符。

    这是修改后的解析调用:

    parse(input.c_str(), *((+alpha_p) >> ' ' >> int_p >> ' ' >> real_p) >> !blank_p);

    第二个参数严格匹配非字母数字字符串,后跟一个空格,后跟一个整数,再跟另一个空格,最后是一个实数。 解析器达到实数后,它将找到一个空格/制表符,然后重新开始与序列匹配或终止。 ! 运算符是指空格/制表符为零或一。 *运算符表示此序列出现零次或多次,因此匹配一个空字符串。

    很容易看出,第二个参数与传统解析器将使用的潜在语法规则之间存在直接关联。 这是当前需求的典型语法规则:

    :S -> (ALPHA INT REAL)*

    标记ALPHA , INT和REAL通常是从词法分析器提供的。 例如, INT被定义为(0-9)+。 有了Spirit,步骤就合并了。

    您怎么知道有什么问题?

    有几种方法可以确定解析器是否出了问题。 最简单的检查方法是测试从parse方法返回的数据结构。 返回数据结构称为parse_info , hit字段指示解析是否成功。 清单3显示了Boost源中的parse_info结构。

    清单3. parse方法返回的parse_info结构
    template <typename IteratorT = char const*> struct parse_info { IteratorT stop; // points to final parse position bool hit; // true when parsing is successful bool full; // when the parser consumed all the input std::size_t length; // number of characters consumed by parser parse_info( IteratorT const& stop_ = IteratorT(), bool hit_ = false, bool full_ = false, std::size_t length_ = 0) : stop(stop_) , hit(hit_) , full(full_) , length(length_) {} template <typename ParseInfoT> parse_info(ParseInfoT const& pi) : stop(pi.stop) , hit(pi.hit) , full(pi.full) , length(pi.length) {} };
    什么是assign_a?

    一旦预定义的解析器匹配了字符串,结果就需要存储在某个地方。 使用assign_a构造,将已解析的字符串分配给相应的变量。 在Spirit框架中分配/修改变量的一般构造类别称为actor ,位于boost / spirit / actor文件夹中。

    精神运算符及其语义

    Spirit具有几个预定义的运算符。 表1总结了这些运算符及其语义。 以下示例使用这些运算符。

    表1. Spirit运算符及其语义
    操作员 语义学 x >> y 匹配x,然后匹配y x | ÿ 匹配x或y x&y 匹配x和y xâ??? ÿ 匹配x但不匹配y X ^ÿ 匹配x或y,但不能同时匹配 *X 匹配x零次或多次 + x 匹配x次或多次 !X 匹配x零或一次 ( X ) 匹配x; 用于基于优先级的分组 x [函数表达式] 如果x匹配,则执行函数/函数 x%y 匹配x一次或多次,以y的出现为间隔

    到目前为止,您已经了解了这一点,现在可以在C中为浮点数定义语法。BNF如清单4所示。

    清单4.浮点数的BNF
    Real-Number : Fractional-Part (Exponent-Part)? Fractional-Part : (DIGIT)* DOT (DIGIT)+ | (DIGIT)+ DOT Exponent-Part : ('e'|'E') ('+'|'-')? (DIGIT)+ DIGIT : ['0'-'9'] DOT : '.'

    清单5中提供了等效的Spirit语法。

    清单5.浮点数的Spirit语法,与清单4中的BNF等效
    Real-Number = Fractional-Part >> ! Exponent-Part | +digit_p >> Exponent-Part ; Fractional-Part = *digit_p >> '.' >> +digit_p | +digit_p >> '.' ; Exponent-Part = ('e' | 'E') >> !('+' | '-') >> +digit_p;

    请注意,在Spirit上下文中, Y = A >> B与解析器上下文中的Y : AB相同,其中A和B可以是终端或非终端。 还要注意,用户不需要为此类琐碎的事情定义语法:Spirit已经具有预定义的parser real_p来解析实数。

    Spirit中的预定义解析器

    Spirit框架的灵活性来自于以下事实:它具有许多针对常见情况的预定义解析器。 表2列出了其中一些解析器。

    表2. Spirit中一些预定义的解析器
    解析器 语义学 ch_p 匹配一个字符。 range_p 匹配从低/高字符对创建的一系列字符中的单个字符。 例如, range_p('a', 'z')匹配a和z之间的所有字符。 任何字符 匹配任何单个字符,包括NULL终止符\0 。 str_p 匹配字符串:例如, str_p("mystring")匹配字符串mystring 。 blank_p 匹配连续的空格和制表符序列。 space_p 与blank_p相似,但也匹配return和换行符。 digit_p 匹配一个数字。 upper_p 匹配任何大写字符。 没有了 诊断工具; 从不匹配任何东西,总是失败。

    精神指令

    本节讨论指令,这是Spirit的另一个强大功能。 不区分大小写的语言(例如Pascal和VHDL)的词法分析器稍微复杂些,因为它们必须解析(例如, begin和BEGin并为解析器生成相同的标记)。 Spirit通过使用解析器指令来实现这一壮举。 例如,预定义指令as_lower_d将输入流转换为小写(请参见清单6)。

    清单6.使用as_lower_d指令进行不区分大小写的解析
    #define BOOST_SPIRIT_NO_REGEX_LIB #include "regex.h" #include "spirit.hpp" #include "boost/spirit/actor/assign_actor.hpp" using namespace std; using namespace boost::spirit; const string input = "THis iS a ranDOm sTRInG"; int main () { string val; int status = parse (input.c_str(), as_lower_d[str_p ("this is a random string") [assign_a(val)] ]).full; cout << status << endl; cout << val << endl; return 0; }

    清单6的输出为1, THis iS a ranDOm sTRInG 。 了解解析器和解析器指令之间的区别很重要-后者仅修改封闭式解析器的行为,从本质上增强了该解析器的策略。

    Spirit提供了其他预定义的解析器指令以及编写一些自己的方法。 现在让我们看一下longest_d解析器指令。 考虑清单7,并尝试猜测输出。

    清单7.使用歧义语法进行解析
    #define BOOST_SPIRIT_NO_REGEX_LIB #include "regex.h" #include "spirit.hpp" #include "boost/spirit/actor/assign_actor.hpp" using namespace std; using namespace boost::spirit; const string input = "20245.1"; int main () { int val; int status = parse (input.c_str(), int_p[assign_a(val)] | real_p).full; cout << status << " " << val << endl; return 0; }

    清单7的输出是0 20245 。 为什么会这样呢? 显然,解析时并没有消耗整个输入缓冲区,这就是为什么status为0 。 要理解这一点,请务必牢记Spirit的解析方式:给定规则示例S : R1 | R2 | .. | RN多个替代项S : R1 | R2 | .. | RN S : R1 | R2 | .. | RN S : R1 | R2 | .. | RN ,左边的那个获得最高优先级。 这类似于C / C ++处理条件的方式:在表达式if (x && y) ,如果x为true,则不对y求值。 此行为有助于使工具保持快速运行。

    在这种情况下, int_p匹配20245-但此后它遇到一个点并且没有规则解析它。 因此,解析器退出。

    您可以通过重新组合语法规则的所有可用替代方案来解决此问题,但是手动重新组合通常容易出错。 解决此问题的更明智的方法是使用longest_d指令,该指令尝试匹配消耗输入流中最大长度的规则。 清单8显示了对parse例程的修改后的调用。

    清单8.使用longest_d预定义的解析器指令
    int status = parse (input.c_str(), longest_d [int_p | real_p[assign_a(val)] ] ).full;

    进行此更改后,输出现在为1 20245.1 。

    在“精神”中发展成熟的语法

    本节深入研究使用Spirit框架设计一组用户定义的语法规则。 这是Spirit设计自己的语法所需要的:

    创建从预定义grammar类继承的派生类。 grammar类是一个模板类,由其派生类DerivedT和上下文类ContextT 。 语法类的声明如下: template< typename DerivedT, typename ContextT = parser_context<> > struct grammar; 您设计的派生类必须具有一个嵌套的模板类/结构,其名称为definition (您不能更改此名称)。 definition类具有以下属性: 这是一个类型为ScannerT的模板类。 语法规则在其构造函数中定义。 向构造函数传递了对实际语法self的引用。 必须存在一个名为start的成员函数。 它表示开始规则。

    清单9显示了用户定义语法的基本框架。

    清单9.用户定义的语法类的概述
    struct my-grammar : public grammar<my-grammar> { template <typename ScannerT> struct definition { rule<ScannerT> startRule; definition(my-grammar const& self) { /* define grammar rules here */ } rule<ScannerT> const& start() const { return startRule; } }; };

    假设您想尝试支持清单10中所示的简单语法,该语法部分解析了C / C ++枚举。

    清单10. C / C ++枚举的简单语法
    enum_specifier : ENUM '{' enumerator_list '}' | ENUM IDENTIFIER '{' enumerator_list '}' | ENUM IDENTIFIER ; enumerator_list : enumerator | enumerator_list ',' enumerator ; enumerator : IDENTIFIER ; ENUM: "enum"; IDENTIFIER: ['a'..'z']+;

    清单11显示了相应的Spirit代码。 程序的输出为1 ,表示分析成功。

    清单11.用于解析C / C ++枚举的Spirit代码
    #define BOOST_SPIRIT_NO_REGEX_LIB #include "regex.h" #include "spirit.hpp" #include "boost/spirit/actor/assign_actor.hpp" using namespace std; using namespace boost::spirit; struct my_enum : public grammar<my_enum> { template <typename ScannerT> struct definition { definition(my_enum const& self) { enum_specifier = enum_p >> '{' >> enum_list >> '}'; enum_p = str_p("enum"); enum_list = +id_p >> *(',' >> +id_p); id_p = range_p('a','z'); } rule<ScannerT> enum_specifier, enum_p, enum_list, id_p; rule<ScannerT> const& start() const { return enum_specifier; } }; }; string input = "enum { ah, bk }"; int main () { my_enum e; int status = parse(input.c_str(), e, space_p).hit; cout << status << endl; return 0; }

    结论

    本文提供了用于分析的Boost Spirit框架的合理详细的初步介绍。 有关更多信息,请参见“ 相关主题”部分。


    翻译自: https://www.ibm.com/developerworks/aix/library/au-boost_parser/index.html

    相关资源:基于Django的一个记事本
    Processed: 0.020, SQL: 9