[yc]boost::spirit初体验

[yc]boost::spirit初体验

    最近为了解析SQL语法,怀着试一试的心态去翻了翻boost的spirit库,因为该库的文档的简介里写着LL parser framework  represents parsers directly as EBNF grammars in inlined C++。看着framework这个词自然觉得这个库很牛B,试用了一下果然如此。
    所谓EBNF即扩展巴克斯范式,是一种描述Context-Free Language的文法。在目前常见的非自然语言中,大部分都可以用EBNF表示。例如:
      group  ::='('exp ')'
      factor ::=integer| group
      term   ::=factor(('*'factor)|('/'factor ))*
      exp    ::=term(('+'term)|('-'term ))*
这是一个整数表达式的EBNF。该段描述用spirit在C++中的实现则是:
   

    rule <>  group, factor, term, exp;
   group  
=   ' ( '   >>  exp  >>   ' ) ' ;
   factor 
=  int_p  |  group;
   term   
=  factor  >>   * (( ' * '   >>  factor)  |  ( ' / '   >>  factor));
   exp    
=  term  >>   * (( ' + '   >>  term)  |  ( ' - '   >>  term));

这里使用=代替::=, 用>>代替空格连接。并且由于C++语法所限,EBNF中后置的*在spirit中改为前置。
等式左边的单词被称为一个rule,等式右边为rule的定义。我们可以看出一个group是一个exp加上一对括号,一个factor是一个整数或者一个group,一个term是一个或多个factor用*/连接,一个exp是一个或多个term用+-连接。处于最顶端的exp可以据此识别出以下表达式
   

    12345
   
- 12345
   
+ 12345
   
1   +   2
   
1   *   2
   
1 / 2   +   3 / 4
   
1   +   2   +   3   +   4
   
1   *   2   *   3   *   4
   (
1   +   2 *  ( 3   +   4 )
   (
- 1   +   2 *  ( 3   +   - 4 )
   
1   +  (( 6   *   200 -   20 /   6
   (
1   +  ( 2   +  ( 3   +  ( 4   +   5 ))))

    得到一个rule之后,我们就可以用 parse函数对一个串进行识别了。例如
         

         parse( " (1 + (2 + (3 + (4 + 5)))) " , exp);


该函数返回一个结构parse_info,可以通过访问其中的full成员来判断是否成功识别,也可以访问stop成员来获知失败的位置。这里要特别提一点,关于各个符号之间的空格,spirit的文档的正文说的是给parse再传一个参数space_p,通知parse跳过所有的空格,然而在FAQ中又提到,如果使用以上方法定义rule,第三个参数传space_p会失败。原因是使用rule默认定义的规则被称为character level parsing,即字符级别解析,而parse的第3个参数仅适用于phrase level parsing,即语法级别解析。要使用第3个参数可以有几种方法。
      1。在parse的第二个参数直接传入一个EBNF表达式,不创建rule对象。
         

            parse( " hello world " * anychar_p, space_p);  


      2。以rule<phrase_scanner_t>创建rule。
         

            rule < phrase_scanner_t >  exp; 

注意虽然可以用这两个办法屏蔽空格,但是这样可能完全改变EBNF文法的语义,尤其是在语言本身需要识别空格的时候。对于这种情况,可以不使用第三个参数,并在需要出现空格的地方加上space_p,或者+space_p及*space_p,其中+和*分别表示后面的符号连续出现一次以上和0次以上。例如一个以空格分隔的整数列表可以写成int_p >> *(+space_p >> int_p)
   如上使用parse可以识别一个串,但并不能做更多的操作,例如将语法里的各个成分提取出来。对于这样的需求,可以通过actor实现。下面是使用actor的一个简单例子
   

    bool
   parse_numbers(
char   const *  str, vector < double >&  v)
   
{
      
return parse(str,

   
//  Begin grammar
      (
         real_p[push_back_a(v)] 
>> *(',' >> real_p[push_back_a(v)])
      )
      ,
      
//  End grammar
      space_p).full;
   }

注意到real_p后面的[],中括号里面是一个仿函数(函数指针或者函数对象),该仿函数具有如下调用型别
   

    void   operator ()(IterT first, IterT last)  const ;
   
void   operator ()(NumT val)  const ;
   
void   operator ()(CharT ch)  const ;


一旦spase发现了匹配real_p的子串,就会调用该functor。不同的rule可能会对应不同的调用型别。
第一个型别针对一般规则,first和last为两个指向字符的迭代器(一般为char*),匹配的子串为[first, last)
第二个型别针对数字型规则,如real_p和int_p, 参数val是一个数字类型。
第三个性别针对单字符型规则,如space_p, 参数ch是一个字符类型。
real_p[push_back_a(v)]中的push_back_a是一个spirit已经定义好的functor,它会将匹配好的内容依照匹配到的时间顺序调用v的push_back函数加入到v中。

   到此spirit的常用功能就都介绍完了。要详细深入了解可以参考spirit的文档。

最后在题一个注意要点。spirit的各种EBNF连接都是指针连接,因此才能在expression被赋值前就在group的定义里面使用。所以在使用EBNF的时候一定要小心不要将局部变量的rule提供给全局或者类成员变量使用,例如:
   

    class  A
   
{
      rule
<> s;
      A()
      
{
         rule
<> r = int_p | hex_p;

         s 
= r >> *(+space_p >> r); //error, r destructed after return 
      }

   }
;

如果真想使用局部作用域,可以在局部的rule前面加上static.

你可能感兴趣的:([yc]boost::spirit初体验)