Boost.Spirit 初体验

使用代码生成代码是一件十分美妙的事情,于是有了各种代码生成器。但是生成代码,意味着要有对生成规则的分析和处理。 Boost.Spirit 就是这么一个语法分析工具,它实现了对上下文无关文法的LL分析。支持EBNF(扩展巴科斯范式)。 Boost.Spirit 的使用真的是把模板嵌套用到了极致。确实这么做造成了非常强的扩展性,生成的代码也非常高效,但是嵌套的太复杂了,对于初学者而言真心难看懂。 你能想象在学习阶段一个不是太明白的错误导致编译器报出的几十层模板嵌套错误信息的感受吗?而且,这么复杂的模板嵌套还直接导致了编译速度的巨慢无比。 其实在之前,我已经使用过Spirit的Classic版本,即1.X版本,但是过多的复制操作让我觉得当时用得很低效,还好分析的内容并不复杂所以没。体现出来 这回就来研究下功能更强劲的2.X 版本。

Boost.Spirit V2 大体上分为三个部分,Qi、Karma和Lex

Qi 库主要是规则生成和解析器,使用方式类似巴科斯范式 Karma 库则是格式化输出工具 Lex 库是类似Flex的规则生成工具,使用正则表达式,某些时候比直接使用Qi更容易看懂一些

注:所有示例的最终运行结果都放在最后

首先来试用Qi库:

Qi库是以解析器Parser为核心的,首先提供了一些基本的解析器,比如整型、字符、浮点数等等。 具体内容参见Boost.Spirit的Qi部分Qi Parsers 章节

Qi还包含属性的定义,参见Qi部分Compound Attribute Rules 章节,属性定义主要是描述了不同的语法规则锁使用的数据结构,帮助我们判断数据转储和读取的。这里面也描述了Qi的解析器支持的操作符。 使用属性定义说明中的操作符、qi::rule和上一条提到的基本解析器,可以组成复杂地满足我们需求的解析规则

另外就是Qi的动作器部分了,见Qi部分Parser Semantic Actions 章节,动作器用于处理匹配玩解析器之后的操作。 申明形式是 parser[f] 其动作函数的形式支持以下格式:

// 如果f是普通或静态函数,f必须满足以下形式之一
void f(Attrib const&);
void f(Attrib const&, Context&);
void f(Attrib const&, Context&, bool&);

// 如果f是仿函数,f必须重载下列操作符之一
void operator()(Attrib const&, unused_type, unused_type) const;
void operator()(Attrib const&, Context&, unused_type) const;
void operator()(Attrib const&, Context&, bool&) const;

// 以上的Attrib都指的是属性器类型

另外,Boost.Spirit还实现了一个Phoenix辅助框架,这是用于生成对类似Lambda表达式的支持的代码的。可以简化很多操作,只要复合Boost.Phoenix的一些规则 这里有个综合Sample:

对于上面代码中的高级生成器,可以参见Boost.SpiritSpirit Repository章节

接下来是Karma库:

这个库是用来把一些STL的数据结构按和Qi一样的规则转化成到输出流的,感觉用处不大,只是一个为了完整而存在的东西。 大体上和Qi差不多,只不过是反过来了。比如,Qi使用的是输入流,Karma使用的是输出流。 另外Karma有一个比较特别的地方,因为规则生成大多数的第一个数据不是Karma组件,所以有个函数karma::eps,用于生成一个空的Karma表达式。 直接上例程吧:

最后是Lex库:

可能有人之前听说过Flex库,用来生成代码的。而Boost.Spirit的Lex库的很多地方和它很像(我也没用过Flex,官方是这么说的)。 Lex的好处呢,就是可以用正则表达式描述一个规则,而且可以动态生成。而且可以可Qi混合起来使用。 在研究这个库的时候,我也同时发现,想要真正高效的使用Spirit库,还应该像这里的例程一样,各种模板继承,但是,这也会增加编程的复杂度。

Lex对并不是支持所有正则表达式的语法,其支持的正则表达式规则可以参见 Lex库Supported Regular Expressions 章节

对于Lex库的规则类型分离,首先可以采用和Flex类似的做法,自定义数据分段处理的仿函数,只要完成

这样的操作符重载即可,在函数中,可以通过不同的ID区分匹配的内容,具体例程下面有

或者,和Qi一样,可以使用扩展的Phoenix功能实现简单的动作器操作

同时,Lex支持命名模式,可以使用lex::lexer::self.add_pattern来创建命名模式和使用{占位符名称}来设置命名占位符的token定义 另外,Lex还可以和Qi结合使用,无论是Lex的模式结构还是按自定义数据分段处理仿函数时使用的ID编号的方法,都有相应的方法让他依据Lex的规则分析,按Qi的动作处理函数处理

Lex还有一个重要的部分,静态规则生成。 静态规则生成比较特殊,首先,生成规则的写法和上面提到的一致,不同的地方有两点首先是lex::lexertl::lexer的使用要改成lex::lexertl::static_lexer 第二处比较特别,就是需要使用lex::lexertl::generate_static_dfa函数,依据规则,生成代码。

上诉所有要点的Sample如下:

Lex的例程比较长,其实很多部分是一样的,但是为了方便观看,就复制了很多遍

各种Sample的运行结果如下:

最后,我觉得要用这个东西的话还是要比较慎重,这里面有大量模板嵌套,一旦出现一点错误极难分析和调试 另外感觉模板使用过度了些,会导致编译速度大幅下降。所以对其还是要有所取舍。如果要做比较复杂的东西,而不是所有成员都对他极为熟悉,还是少用为妙。 以上都是个人观点和学习记录,如果有不对的地方还请指正。

Last updated

Was this helpful?