12.3. 语法
继上一节中以 EBNF 为 JSON 格式定义了相关规则后,现在要将这些规则与 Boost.Spirit 一起使用。 Boost.Spirit 实际上允许以 C++ 代码来定义 EBNF 规则,方法是重载各个由 EBNF 使用的不同操作符。
请注意,EBNF 规则需要稍作修改,才能创建出合法的 C++ 代码。 在 EBNF 中各个符号是以空格相连的,在 C++ 中需要用某个操作符来连接。 此外,象星号、问号和加号这些操作符,在 EBNF 中是置于对应的符号后面的,在 C++ 中必须置于符号的前面,才能作为单参操作符来使用。
以下是在 Boost.Spirit 中为表示 JSON 格式,用 C++ 代码写的 EBNF 规则。
- #include <boost/spirit.hpp>
- struct json_grammar
- : public boost::spirit::grammar<json_grammar>
- {
- template <typename Scanner>
- struct definition
- {
- boost::spirit::rule<Scanner> object, member, string, value, number, array;
- definition(const json_grammar &self)
- {
- using namespace boost::spirit;
- object = "{" >> member >> *("," >> member) >> "}";
- member = string >> ":" >> value;
- string = "\"" >> *~ch_p("\"") >> "\"";
- value = string | number | object | array | "true" | "false" | "null";
- number = real_p;
- array = "[" >> value >> *("," >> value) >> "]";
- }
- const boost::spirit::rule<Scanner> &start()
- {
- return object;
- }
- };
- };
- int main()
- {
- }
为了使用 Boost.Spirit 中的各个类,需要包含头文件 boost/spirit.hpp
。 所有类均位于名字空间 boost::spirit
内。
为了用 Boost.Spirit 创建一个词法分析器,除了那些定义了数据是如何构成的规则以外,还必须创建一个所谓的语法。 在上例中,就创建一个 json_grammar
类,它派生自模板类 boost::spirit::grammar
,并以该类的名字来实例化。 json_grammar
定义了理解 JSON 格式所需的完整语法。
语法的一个最重要的组成部分就是正确读入结构化数据的规则。 这些规则在一个名为 definition
的内层类中定义 - 这个名字是强制性的。 这个类是带有一个模板参数的模板类,由 Boost.Spirit 以一个所谓的扫描器来进行实例化。 扫描器是 Boost.Spirit 内部使用的一个概念。 虽然强制要求 definition
必须是以一个扫描器类型作为其模板参数的模板类,但是对于 Boost.Spirit 的日常使用来说,这些扫描器是什么以及为什么要定义它们,并不重要。
definition
必须定义一个名为 start()
的方法,它会被 Boost.Spirit 调用,以获得该语法的完整规则和标准。 这个方法的返回值是 boost::spirit::rule
的一个常量引用,它也是一个以扫描器类型实例化的模板类。
boost::spirit::rule
类用于定义规则。 非终结符号就以这个类来定义。 前面所定义的非终结符号 object
, member
, string
, value
, number
和 array
的类型均为 boost::spirit::rule
。
所有这些对象都被定义为 definition
类的属性,这并非强制性的,但简化了定义,尤其是当各个规则之间有递归引用时。 正如在上一节中所看到的 EBNF 例子那样,递归引用并不是一个问题。
乍一看,在 definition
的构造函数内的规则定义非常类似于在上一节中看到的 EBNF 生成规则。 这并不奇怪,因为这正是 Boost.Spirit 的目标:重用在 EBNF 中定义的生成规则。
由于是用 C++ 代码来组成 EBNF 中建立的规则,为了写出合法的 C++,其实是有一点点差异的。 例如,所有符号间的连接是通过 >>
操作符完成的。 EBNF 中的一些操作符,如星号,被置于相应符号的前面而非后面。 尽管有这样一些语法上的修改,Boost.Spirit 还是尽量在将 EBNF 规则转换至 C++ 代码时不进行太多的修改。
definition
的构造函数使用了由 Boost.Spirit 提供的两个类:boost::spirit::ch_p
和 boost::spirit::real_p
。 这些以分析器形式提供的常用规则可以很方便地重用。 一个例子就是 boost::spirit::real_p
,它可以用于保存正或负的整数或浮点数,无需定义象 digit
或 real
这样的非终结符号。
boost::spirit::ch_p
可用于创建一个针对单个字符的分析器,相当于将字符置于双引号中。 在上例中,boost::spirit::ch_p
的使用是强制性的,因为波浪号和星号是要应用于双引号之上的。 没有这个类,代码将变为 *~"\""
,这会被编译器拒绝为非法代码。
波浪号实际上是实现了前一节中提到的一个技巧:在双引号之前加上波浪号,可以接受除双引号以外的所有其它字符。
定义完了识别 JSON 格式的规则后,以下例子示范了如何使用这些规则。
- #include <boost/spirit.hpp>
- #include <fstream>
- #include <sstream>
- #include <iostream>
- struct json_grammar
- : public boost::spirit::grammar<json_grammar>
- {
- template <typename Scanner>
- struct definition
- {
- boost::spirit::rule<Scanner> object, member, string, value, number, array;
- definition(const json_grammar &self)
- {
- using namespace boost::spirit;
- object = "{" >> member >> *("," >> member) >> "}";
- member = string >> ":" >> value;
- string = "\"" >> *~ch_p("\"") >> "\"";
- value = string | number | object | array | "true" | "false" | "null";
- number = real_p;
- array = "[" >> value >> *("," >> value) >> "]";
- }
- const boost::spirit::rule<Scanner> &start()
- {
- return object;
- }
- };
- };
- int main(int argc, char *argv[])
- {
- std::ifstream fs(argv[1]);
- std::ostringstream ss;
- ss << fs.rdbuf();
- std::string data = ss.str();
- json_grammar g;
- boost::spirit::parse_info<> pi = boost::spirit::parse(data.c_str(), g, boost::spirit::space_p);
- if (pi.hit)
- {
- if (pi.full)
- std::cout << "parsing all data successfully" << std::endl;
- else
- std::cout << "parsing data partially" << std::endl;
- std::cout << pi.length << " characters parsed" << std::endl;
- }
- else
- std::cout << "parsing failed; stopped at '" << pi.stop << "'" << std::endl;
- }
Boost.Spirit 提供了一个名为 boost::spirit::parse()
的自由函数。 通过创建一个语法的实例,就会相应地创建一个词法分析器,该分析器被作为第二个参数传递给 boost::spirit::parse()
。 第一个参数表示要进行分析的文本,而第三个参数则是一个表明在给定文本中哪些字符将被跳过的词法分析器。 为了跳过空格,要将一个类型为 boost::spirit::space_p
的对象作为第三个参数传入。 这只是表示在被捕获的数据之间 - 换句话说,就是规则中使用了 >>
操作符的地方 - 可以有任意数量的空格。 这其中包含了制表符和换行符,令数据的格式可以更为灵活。
boost::spirit::parse()
返回一个类型为 boost::spirit::parseinfo
的对象,该对象提供了四个属性来表示文本是否被成功分析。 如果文本被成功分析,则属性 _hit 被设置为 true
。 如果文本中的所有字符都被分析完了,最后没有剩余空格,则 full 也被设置为 true
。 仅当 hit 为 true
时,length 是有效的,其中保存了被成功分析的字符数量。
如果文本未能分析成功,则属性 length 不能被访问。 此时,可以访问属性 stop 来获得停止分析的文本位置。 如果文本被成功分析,stop 也是可访问的,只不过没什么意义,因为此时它肯定是指向被分析文本之后。