小试 boost spirit
解释文本文件是日常编程中太平常的一件事情了,一般来说,土鳖点的做法可以直接手写 parser 用循环暴力地去 map 文本上的关键字从而提取相关信息,想省力一点则可以使用 tokenizer 或正则表达式之类的工具,无论怎样,总的来说,手写 parser 去解释文本基本是件苦力活:写出的代码比较难重用,可读性可维护性也差,要是设计的差点,哪天文本格式一变,以前辛苦写的代码马上推倒重来,未尝是新鲜事。
解救的方法是通过工具来生成 parser,这方面有好多比较出名的工具,比如 Lex, Yacc, ANTLR 等,它们的功能大同小异,都基于文法(用 EBNF 进行描述) 转换成相应的 parser. 但这些工具使用起来通常比较麻烦:首先是生成的代码可读性不大好,再者如果要把这些代码加入到工程中,通常都需要对生成的代码加以修改,因此文法一旦发生变动,parser 需要再次生成,这些修改基本又都得再次重来,因此维护困难。
那么除了上述这些工具还有没有别的解决方案呢?也许你可以尝试一下 boost spirit。
以下内容基于 boost 1.37.0,spirit 的版本是 1806,比较老的一个版本,与最新版本相比,大概原理虽是差不多,但库的目录结构已有很大不同,知悉。
Spirit 是什么
简单来说,Spirit 是一个 parser generator,功能与 Yacc,ANTLR 类似,且也是基于 EBNF 来描述文法,再基于文法生成 parser,但与前面这些工具相比,它最大的不同点在于它使用了 C++ 代码来对文法进行描述,通过非常残暴的模板编程技巧,在编译阶段就生成了相应的 parser。从使用者的角度来看,文法是用代码进行描述的,因此它天生就能直接加入到你当前的工程中与现成代码揉合在一起。
当然,Spirit 的文法在形式上 EBNF 还是有一点点的出入,比如说,用 ">>" 来连接不同表达式,表示重复的符号放在了表达式的前面等,这些都是受 c++ 语法的限制所做出的折衷,文法的语义其实未变。
初体验
一个经典的整数四则运算如用 EBNF 来描述的话,可以写成如下的形式:
group ::= '(' expression ')'
factor ::= integer | group
term ::= factor (('*' factor) | ('/' factor))*
expression ::= term (('+' term) | ('-' term))*
其中 group, factor, term, expression 分别称为一个 rule,用于表示怎么去匹配一条相应的文本,上述 EBNF 文法在 Spirit 中可以写成如下形式:
group = '(' >> expression >> ')';
factor = integer | group;
term = factor >> *(('*' >> factor) | ('/' >> factor));
expression = term >> *(('+' >> term) | ('-' >> term));
看起来语法好像差不多,只是运算符有些不同,需要指明的是,在 Spirit 中一个 rule 就是一个 parser 对象,一个 parser 对象包含了相应的语法规则使得该 parser 只能 parse 符合这些规则的文本,比如说,我们现在想 parse 出一组用逗号隔开的整数(CSV),则我们可以定义如下一个 rule:
boost::spirit::rule<> csv_int = int_p >> *(',' >> int_p);
上述代码中,int_p 是 Spirit 内建的一个 rule 或者说 parser,该 parser 专门用于 parse 一个整型(除了 int_p,Spirit 还内建了一系列用于 parse 其它基本数据类型的 parser, 具体列表参考这里)。parser 与 parser 通过 ">>" 运算符连接在一起后就组成了一个新的 parser,那么怎么来使用 csv_int 这个新生成的 parser 呢? Spirit 内建定义了一个函数,原型大概如下:
boost::spirit::parse_info<> parse(const char* text, parser, separator);
通过调用 parse() 函数传入需要 parse 的文本与相应的 parser 就能对该文本进行相应的解释,返回结果会指明 parse 的过程是否成功了,及如果出错,在哪个位置出错了。
Semantic actions
前面定义的 csv_int 这个 parser 虽然定义了文法,但它基本没做什么事情,只能用来检查一下某段文本是不是一组逗号隔开的整型,功能显然太弱了,因为通常来说,我们是需要从文本中提取出数据来的,因此 parse 的过程需要支持某些动作,我们需要 parser 在 parse 到某些内容时,能够执行用户指定的行为动作,在 Spirit 中,这个些动作就叫作 semantic action.
Semantic action 是属于一个 parser 的,它的意义在于指明当该 parser 执行成功了之后,要执行哪些操作,而这些操作是由用户指定的。我们可以通过如下方式将一个 semantic action 与一个 parser 联系起来:
parser[func];
至于 func 的原型,当然是有要求的,而且这个要看具体的 parser, 比如说 int_p 这样的 parser,它就只能接受 void func(const int val);
这样的函数,很简洁的语法!现在我们来在将前面用于解释一组整型的代码中加入一个新功能,在 parse 完每一个整型后,我们将得到的数据保存下来。
#include <vector>
#include <boost/spirit.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/include/phoenix_object.hpp>
std::vector<int> g_output;
int on_parse_int(const int val)
{
g_output.push_back(val);
}
int main()
{
boost::spirit::rule<> int_csv_rule = int_p[on_parse_int] >> *(',' >> int_p[on_parse_int]);
boost::spirit::parse("2,3,4", int_csv_rule);
return 0;
}
semantic action 的实现依赖于 boost 里另一个名声显赫的函数式模板库:phoenix, 上面的例子只是一个简单示范,未及冰山一角,spirit 其实还支持用 lambda 来写回调函数,以及用闭包来在不同的 rule 之间传递用户定义的上下文信息等,功能很强大,有兴趣的读者可以参考下这里相对完整点的一个例子,它实现了一个简单的四则运算及基本的函数调用。
生成的 Parser 的类型
Parser 是整个 Spirit 库的核心功能所在,那么 Spirit 生成的 parser 是怎么进行工作呢? 结论是,spirit 所生成的 parser 就是所谓的 LL recursive decent parser,因此 parse 的时候是从左往右扫描输入,而对 parser 中的文法,采取先左后右的顺序进行匹配的,因此左递归之类的问题需要使用者自己消除,对如下一个例子:
rule<> rule1 = (int_p >> ',' >> int_p) | real_p;
rule1 在 parse 文本时,会优先匹配 (int_p >> ',' >> int_p),如若失败,则再去匹配 real_p。
优缺点
因为使用该库的时间还不是很长,初步上手的感觉,优点上个人觉得有如下几点:
- 使用非常方便,尤其当要 parse 一些不太复杂的文本时,写代码的效率很高。
- 生成的 parser 执行效率也很好。
结论就是:很好很强大,但与此同时,缺点也明显:
- 错误提示不够友好。当一段文本格式上有错误时,spirit 直接从出错的地方返回但却不提供相应的错误信息,上层的代码只知道在哪里出了错,却不知道是因什么出了错,因此很难生成一个有意义的错误提示。
- 该库的实现大量使用了模板及符号重载,代码写起来很酷炫,但是一旦出错,错误提示基本没有包含太多有意义的信息,因此调试起来很痛苦,尤其是在使用不够熟练的情况下,因此个人建议在使用 Spirit 时,最好把任务进行适当分解,每完成一个小的任务就先测试确认它功能正常稳定再进行下一步,不要一下子写一大堆代码,先不说功能如不正常难以调试,甚至编译错误时,找到出错的地方都困难。
- 该库在实现上严重依赖模板元编程,使用了诸多如 expression template 这样的 coding idiom,parser 的生成实际上是在编译阶段完成,因此当你写的 rule 很复杂时,编译时间会很长,真的很长。
【参考】
http://boost-spirit.com/distrib/spirit_1_8_3/libs/spirit/doc/quick_start.html
http://en.highscore.de/cpp/boost/
小试 boost spirit的更多相关文章
- boost::spirit unicode 简用记录
本文简单记录使用boost::spirit解析有中文关键字的字符串并执行响应动作,类似于语法分析+执行. 关键字:字符串解析 boost::spirit::qi::parse qi::unicode: ...
- 在msvc中使用Boost.Spirit.X3
Preface “Examples of designs that meet most of the criteria for "goodness" (easy to unders ...
- 使用BOOST.SPIRIT.X3的RULE和ACTION进行复杂的语法制导过程
Preface 上一篇简述了boost.spirit.x3的基本使用方法.在四个简单的示例中,展示了如何使用x3组织构造一个语法产生式,与源码串匹配并生成一个综合属性.这些简单的示例中通过组合x3库中 ...
- boost.spirit之解析C++头文件
环境:win7_64旗舰版,VS2008 场景:C++与lua交互是比较繁琐的,当我们编写一个C++类后,如果要给lua使用,就必须写一个lua包装类,将lua与原始C++类关联起来.其实这部分代码编 ...
- ceph 源码安装 configure: error: "Can't find boost spirit headers"
问题:configure: error: "Can't find boost spirit headers" 解决: 推荐:sudo apt-get install libboos ...
- boost之词法解析器spirit
摘要:解析器就是编译原理中的语言的词法分析器,可以按照文法规则提取字符或者单词.功能:接受扫描器的输入,并根据语法规则对输入流进行匹配,匹配成功后执行语义动作,进行输入数据的处理. C++ 程序员需要 ...
- Boost学习之语法解析器--Spirit
Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C ...
- Boost程序库完全开发指南——深入C++“准”标准库(第3版)
内容简介 · · · · · · Boost 是一个功能强大.构造精巧.跨平台.开源并且完全免费的C++程序库,有着“C++‘准’标准库”的美誉. Boost 由C++标准委员会部分成员所设立的Bo ...
- 《超越C++标准库:Boost库导引》:序
序(Foreword) C++社区正在发生着一些美妙的事情.尽管C++仍然是世界上使用最广泛的编程语言,它依旧在变得更加强大而且易用.不信么?容我慢慢道来. 当前版本的标准C++是在1998年最终确定 ...
随机推荐
- JPA中建立数据库表和实体间映射小结
在JPA中,映射数据库表和实体的时候,需要注意一些细节如下, 实体类要用@Entity的注解: 要用 @Id 来注解一个主键: 如果跟数据库相关联,要用@Table注解相关数据库表: 实体类中字段需要 ...
- Linux命令:pwd
打印当前目录的完全路径. -L 打印路径包含符合路径 -P 打印路径不含符合路径. -LP,可能打印的不同,取决于你对进入当前目录的方式是通过符号链接进入,还是物理目录进入.如果是符号链接进入,则-L ...
- CSS: Grid homework redact.
The web homework: Finished design: (I use six block with different color to show this homework and I ...
- Linux Oracle安装
lsnrctl status // 查看linux系统oracle的监听状态lsnrctl start // 启动linux系统oracle的监听状态 sqlplus /nolog // 连接 ...
- jq 切换功能toggle
---恢复内容开始--- $(document).ready(function () { $(".jianjie").click(function () { $(this).tog ...
- 用js控制 给一个input赋值之后,change事件不能捕获到,解决办法
你用js给input赋值后要调用change方法 下面是jquery的写法 $('input#3').val("50"); $('input#3').change(); 自己试试吧
- Cdnbest的cdn程序默认支持web Socket
Cdnbest的cdn程序默认支持web Socket WSS 是 Web Socket Secure 的简称, 它是 WebSocket 的加密版本. 我们知道 WebSocket 中的数据是 ...
- OpenStack 安装:nova服务
上一篇介绍了glance,并且成功创建了一个镜像,这一篇介绍Nova. 首先创建Nova用户,需要记得先source环境变量,然后创建Nova用户,并设置密码为nova [root@linux-nod ...
- linux 大容量磁盘分区工具parted
1. Msdos和Gpt的区别 fdisk :只能分msdos分区parted :可以分msdos和gpt分区 2. MSDOS特点最大支持2TB卷大小.每个磁盘最多只能有4个主分区(或3个主分区, ...
- 二十一、proxyDesign 代理模式
原理: 时序图: 代码清单: Printable public interface Printable { void setPrinterName(String name); String getPr ...