Andrew's Blog

Make things as simple as possible, but no simpler -- Albert Einstein

导航

boost.spirit库--文档翻译计划--(3)快速起步

Posted on 2005-01-16 22:47  andrew  阅读(2080)  评论(2)    收藏  举报

快速起步

为什么你需要使用Spirit?

Spirit被设计成实用的解析工具。从嵌入到C++中的EBNF形式说明生成正常工作的解析器之能力至少可以极大地缩短开发时间。虽然当我们想要开发一门像C或Pascal那样的计算机语言时,会选用类似YACC或ANTLR这样强大而独立的解析器生成工具;但是当我们希望编写一个极其微小的解析器时,该方法总给人一种“杀鸡焉用牛刀”的感觉。在这种情况下,程序员通常不会将手头的工作当成正式的解析任务,而是用原始的工具(比如scanf)对其进行专门地处理。诚然,像正则表达式程序库(如boost regex)或扫描器(如boost tokenizer)这样的工具是存在的。然而当我们需要编写更为精巧的解析器时,这些工具就显得有些力不从心了。如果仍试图使用这些工具来编写中等复杂程度的解析器,则会导致难于理解和维护的代码。

首要目标是使工具得心应手。当大家想到解析器的生成器时,常见的反应是“它一定庞大而复杂,且具有陡峭学习曲线”。但是Spirit并非如此,它被设计为完全是可伸缩的。该框架具有分层结构。这使得在仅仅学习了最小核心和基本概念之后,“按需学习Spirit”成为可能。

为了便于开发和简化部署,整个框架仅包含头文件,而没有任何需要链接或生成的库。你只需将Spirit发布版本放到包含(include)路径下、编译和运行即可。代码的规模是多大?非常紧凑。在接下来将要介绍的快速起步示例中,代码的规模主要被std::vector和std::iostream的实例化所占据。请注意:作为Spirit的约定,提供给用户直接使用的解析器的名称均以“_p”结尾。Spirit拥有许多预定义的解析器,遵守命名约定可帮助你避免陷入混乱。

简单示例1

创建一个能够解析浮点数的解析器:

    real_p

(必须承认,此例相当简单)上述代码实际上生成了一个Spirit中的real_parser(一个内置解析器),它可以解析一个浮点数。

简单示例2

创建一个解析器,它可以接受含有两个浮点数的一行文字。

    real_p >> reak_p

你可以看到此处使用了两次我们熟悉的浮点数解析器,对每个数字各使用一次。那么这里的>>运算符有何用处呢?啊!原来解析器必须被分隔开来,因此我们选择该运算符作为表示“跟随”含义的连接运算符。上面的程序段通过连接运算符将两个简单的解析器“胶合”在一起,从而创建出一个新的解析器。也就是说,这个新解析器是由较小的解析器组合而成的。根据解析器的调用方式,两个数字之间的空格符可被自动忽略掉(请参见下文)。

注意:当我们组合解析器时,最终会得到一个“更大”的解析器,但它仍然是一个解析器。解析器可越变越大,嵌套的层数可越来越多,但是无论何时你将两个解析器连接在一起,你都会得到一个更大的解析器。这是一个重要的概念。

简单示例3

创建一个解析器,它可以接受任意多个浮点数。(“任意”表示从零个到无穷多个)
    *real_p
这很像正则表达式中的克林星号,虽然在C++程序员看来,此语法显得有些怪异,因为他们不习惯于将*运算符像这样进行重载。实际上,如果你了解正则表达式,那么你同样会感到奇怪,因为星号跑到了它所修饰的表达式之前。生活就是这样!要怪就只能怪我们必须遵循C++语法规则这个事实了。

克林星号可作用于任何求值结果为解析器的表达式。请记住,由于C++的运算符优先级规则,你需要将复合表达式放到括号中。克林星号也称为克林闭包,但在大多数地方我们都把它叫做“星号”。

示例4 [一个稍微大一些的例子]

本例将创建一个解析器,它接受由逗号分隔的数字列表,并将这些数字放进一个向量(vector)中。

第1步:创建解析器

    real_p >> *(ch_p(',') >> real_p)

请注意ch_p(',')。它是一个能够识别逗号‘,’的字符解析器。在这种情况下,克林星号修饰了一个更为复杂的解析器,即由下面的表达式生成的解析器:
   
    (ch_p(',') >> real_p)

注意此处的括号是必需的。克林星号包围着上述整个表达式。

第2步:使用解析器(既然已经创建出来了)

既然我们已经创建出一个解析器,那么怎样使用它呢?像C++中任何临时对象的结果一样,我们既可以将其保存在一个变量中,也可以直接对其调用函数。

我们将略过一些低级的C++细节,而仅讨论关键内容。

如果r是一条规则(目前不必考虑规则的准确定义是什么,这将在后文讨论。这里我们只将规则理解为一个能够保存解析器的占位变量就已经足够了。),那么我们像这样将解析器保存为一条规则:
    r = real_p >> *(ch_p(',') >> real_p);
这并不算新奇,它与你多年来使用的任何一个C++赋值表达式是一样的。将解析器保存为一条规则的重要意义在于:规则本身就是解析器,你可以通过名称来引用它。(在本例中,名称是r)。请注意:现在它是一条完整的赋值表达式语句,因此我们要使用分号“;”作为语句的结束标志。

好了。我们已经完成了解析器的定义。下一步就是调用该解析器,让它完成自己的工作。有许多方法可以做到这一点。但现在我们将使用接受char const*的全局函数parse。该函数接受三个实参:
以空字符(null)结束的const char*输入
解析器对象
另一个叫做跳过解析器(skip parser)的解析器对象

在本例中,我们希望跳过(即忽略)空格符和制表符。在Spirit的预定义解析器仓库中,有一个名为space_p的解析器。它是一个仅识别空格符的极为简单的解析器。我们将使用space_p作为跳过解析器。跳过解析器是用于忽略出现在两个解析器元素(比如real_p和ch_p)之间的字符的解析器。

好了,现在让我们开始解析吧:

    r = real_p >> *(ch_p(',') >> real_p);
    parse(str, r, space_p)  // 现在仍不是一条完整的语句,请耐心等待...

解析函数parse将返回一个对象(叫做parse_info),其中含有解析过程的结果信息(还有一些其他信息)。在本例中,我们需要知道:

解析器是否成功地识别了输入字符串str?
解析器是否完整地解析并消耗到输入的结尾。

为了对我们目前的工作有一个完整的印象,让我们用一个函数来包装这个解析器:
bool
parse_numbers(char const* str)
{
    return parse(str, real_p >> *(',' >> real_p), space_p).full;
}

请注意,在此处我们没有使用命名的规则(rule),而是直接将解析器嵌入到parse函数的调用参数中。当调用parse函数时,对该表达式进行求值,从而产生一个临时的无名解析器对象,它被传递到parse()函数中,然后是使用它,最后销毁它。
===========================================================================
char 和wchar_t操作数

细心的读者可能会发现在解析器表达式中包含的是',',而不是前例中的ch_p(',')。根据C++的类型转换规则这样做是可以的。Spirit中存在着对 >> 运算符的重载函数,以接受char或wchar_t类型的实参作为其左操作数或右操作数(但不是两个操作数)。如果某个运算符的参数中至少有一个是用户定义类型的,则该运算符可以被重载。在本例中,real_p是operator>>的第二个实参,因此编译器会使用>>运算符合适的重载版本,将','转换为字符解析器。

当省略ch_p调用时,应该注意一个问题:'a' >> 'b'并不是一个spirit解析器,它是一个数值表达式,会将字符'a'的ASCII(或其他的编码)码值按二进制位右移,移动的位数等于字符'b'的ASCII码值。但是,ch_p('a') >> 'b' 和 'a' >> ch_p('b')都是用于解析字母'a'后面跟随着字母'b'的合法spirit连接解析器。你很快就会习惯这些的。
============================================================================
注意由parse函数返回的对象有一个名为full的成员,当我们上述的两个要求都得到满足时(即解析器完整的解析了输入串),full的值为true。

第3步:语义动作

我们前面讨论的解析器仅仅是一个识别程序而已。它只是回答了“输入串符合我们给定的语法吗?”这样的问题,但是它既没有记住任何数据,也没有执行任何动作。还记得吗:我们想要将解析出来的数字放到一个向量中。这项工作应该在与某个特定解析器链接的动作(action)中完成。例如,当我们解析一个实数时,我们总是希望在成功匹配之后,将解析出的数字保存起来。现在我们希望从解析器中提取信息,语义动作所完成的正是这项工作。语义动作可以被附加到文法说明的任何位置。这些语义动作是C++中的函数或仿函数(functor),只要解析器的一个部件成功地识别了输入串的一部分,它们就会被调用。比如说,你有一个解析器P,和一个C++函数F,你可以像这样将F附加到解析器P,从而使解析器在匹配了输入串之后就调用函数F:

P[&F]

或者,如果F是一个函数对象(仿函数),则使用:

P[F]

函数或仿函数的签名(signature)由它所附加到的解析器的类型来决定。解析器real_p只传递一个实参,即被解析出的数字。因此,如果我们要将一个函数F附加到real_p,那么我们需要这样声明F:

void F(double n);

但是对于本例来说,我们可以利用一些预定义的仿函数和仿函数生成器(仿函数生成器是一个返回仿函数的函数)。Spirit提供的仿函数生成器push_back_a(c)恰好能够完成我们的要求。简言之,该语义动作在调用时会将已解析出的数值添加到容器c的末尾,而这个数值是从该语义动作所附加到的解析器接收得到的。

最后,我们给出由逗号分隔的浮点数列表解析器的完整代码清单:

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;
}

这里给出的解析器与前面的相同。但是这次我们将恰当的语义动作附加到了合适的位置,用以提取解析出的数字,并将它们放到向量v中。当解析成功完成时,parse_numbers函数将返回true。

在此处可以查看到全部源代码。它是Spirit发行包的一部分。

版权所有 (c) 1998-2003 Joel de Guzman

使用、修改和发行应遵守Boost软件许可证,版本1.0.(参见附带文件LICENSE_1_0.txt或者位于http://www.boost.org/LICENSE_1_0.txt处的副本)。