解析器以 string
作为输入,它代表模板元程序中的一个字符串。例如,字符串 "Hello World!"
可以按以下方式定义:
string<'H','e','l','l','o',' ','W','o','r','l','d','!'>
这种语法使得解析器的输入难以阅读。Metaparse 使用 C++98 编译器,但解析器的输入必须按上述方式定义。
基于 C++11 提供的 constexpr
特性,Metaparse 提供了一个宏 BOOST_METAPARSE_STRING
用于定义字符串。
BOOST_METAPARSE_STRING("Hello World!")
这也定义了一个 string
,但它更容易阅读。以这种方式可以定义的字符串的最大长度是有限的,但这个限制是可配置的。它由 BOOST_METAPARSE_LIMIT_STRING_SIZE
宏指定。
错误使用编译时数据结构来描述。它包含有关检测到错误的位置以及一些关于错误的 描述。可以使用 debug_parsing_error
来显示错误消息。Metaparse 提供 BOOST_METAPARSE_DEFINE_ERROR
宏来定义简单的 解析错误消息。
可以通过组合简单解析器来构建复杂的解析器。解析器库包含许多解析器组合子,它们可以根据现有解析器构建新解析器。
例如,accept_when
<Parser, Predicate, RejectErrorMsg>
是一个解析器。它使用 Parser
来解析输入。当 Parser
拒绝输入时,组合子会返回 Parser
失败的错误。当 Parser
成功时,组合子会使用 Predicate
验证结果。如果谓词返回 true,则组合子接受输入;否则,它会生成一个带有 RejectErrorMsg
消息的错误。
有了 accept_when
,就可以使用 one_char
来构建只接受数字字符、只接受空白字符等的解析器。例如,digit
只接受数字字符。
typedef boost::metaparse::accept_when< boost::metaparse::one_char, boost::metaparse::util::is_digit, boost::metaparse::errors::digit_expected > digit;
成功解析的结果是某个值以及未解析的剩余字符串。剩余字符串可以由另一个解析器处理。解析器库提供了一个解析器组合子 sequence
,它接受多个解析器作为参数,并从它们构建一个新解析器,该解析器:
解析未知长度的列表是很常见的。例如,我们从一个简单的例子开始:文本是数字列表。例如:
11 13 3 21
我们希望解析结果是这些值的总和。Metaparse 提供 int_
解析器,我们可以用它来解析其中一个数字。Metaparse 提供 token
组合子来消耗数字后面的空白。因此,以下解析器解析一个数字及其后面的空白:
using int_token = token<int_>;
解析结果是装箱的整数值:已解析数字的值。例如,解析 BOOST_METAPARSE_STRING
("13 ")
的结果是 boost::mpl::int_<13>
。
我们的示例输入是数字列表。每个数字都可以由 int_token
解析。
此图显示了重复应用 int_token
如何解析示例输入。Metaparse 提供 repeated
解析器来实现这一点。解析结果是一个类型列表:各个数字的列表。
此图显示了 repeated
<int_token>
的工作原理。它重复使用 int_token
解析器,并从它提供的结果构建一个 boost::mpl::vector
。
但我们需要这些的总和,所以我们需要汇总结果。我们可以通过将我们的解析器 repeated
<int_token>
包装在 transform
中来实现。这为我们提供了指定一个函数来将此类型列表转换为另一个值—在本例中是元素总和—的机会。起初,让我们忽略如何汇总向量中的元素。假设它可以由 lambda 表达式实现,并使用 boost::mpl::lambda<...>::type
来表示该 lambda 表达式。这是一个使用 transform
和此 lambda 表达式的示例:
using sum_parser = transform< repeated<int_token>, boost::mpl::lambda<...>::type >;
transform
<>
解析器组合子包装了 repeated
<int_token>
以构建我们需要的解析器。下面是它如何工作的图示:
如图所示,transform
<
repeated
<int_token>, ...>
解析器使用 repeated
<int_token>
解析输入,然后对解析结果进行一些处理。
让我们来实现缺失的 lambda 表达式,它告诉 transform
如何更改来自 repeated
<int_token>
的结果。我们可以通过使用 Boost.MPL 的 fold
或 accumulate
来汇总类型列表中的数字。下面是一个例子:
using sum_op = mpl::lambda<mpl::plus<mpl::_1, mpl::_2>>::type; using sum_parser = transform< repeated<int_token>, mpl::lambda< mpl::fold<mpl::_1, mpl::int_<0>, sum_op> >::type >;
这是上面图示的扩展版本,显示了这里发生的情况:
此示例解析输入,构建数字列表,然后遍历它并汇总值。它从 fold
的第二个参数 int_<0>
开始,然后逐个添加数字列表(这是解析器 repeated
<int_token>
的结果)中的每个项。
这样做虽然有效,但效率不高:它有一个循环,逐个解析整数,构建一个类型列表,然后遍历这个类型列表来汇总结果。在应用程序中使用模板元程序可能会严重影响编译器的内存使用和编译速度,因此我建议谨慎处理这些事情。
Metaparse 提供了更有效的方法来实现相同的结果。您不需要两个循环:您可以将它们合并,并在解析每个数字后立即将其添加到摘要中。Metaparse 提供了 foldl
来实现此目的。
使用 foldl
,您可以指定:
int_token
)int_<0>
)sum_op
)我们的解析器可以这样实现:
using better_sum_parser = foldl<int_token, mpl::int_<0>, sum_op>;
如您所见,解析器的实现更紧凑。下面是使用此解析器解析输入时发生情况的图示:
如您所见,不仅解析器的实现更紧凑,而且它通过减少工作量也达到了相同的结果。它像以前的解决方案一样,通过重复应用 int_token
来解析输入。但它在内部没有构建类型列表的情况下生成最终结果。这是它的内部工作原理:
它使用 sum_op
来汇总重复的 int_token
应用结果。此实现更有效。它接受空字符串作为有效输入:其总和为 0
。这可能对您有益,在这种情况下,您就完成了。如果您不想接受它,可以使用 foldl1
代替 foldl
。这相同,但它会拒绝空输入。(如果您选择第一种方法并想拒绝空字符串,Metaparse 也提供 repeated1
)
![]() |
注意 |
---|---|
注意,如果您是第一次阅读本手册,您可能想跳过本节,然后继续阅读 介绍 foldl_start_with_parser。 |
您可能已经注意到 Metaparse 也提供了 foldr
。 foldl
和 foldr
之间的区别在于汇总结果的方向。(l
代表 从左,r
代表 从右)下面是使用 foldr
实现 better_sum_parser
的图示:
如您所见,这与使用 foldl
非常相似,但来自 int_token
单独应用的汇总结果是按从右到左的顺序进行的。由于 sum_op
是加法,它不会影响最终结果,但在其他情况下可能会。
正如您可能预料的那样,Metaparse 也提供 foldr1
,它从右侧折叠并拒绝空输入。
让我们改变一下我们的小语言的语法。与其说是数字列表,不如说我们期望数字用 +
符号分隔。我们的示例输入变成如下:
BOOST_METAPARSE_STRING("11 + 13 + 3 + 21")
使用 foldl
或 repeated
解析它很困难:在每个元素除了第一个元素之前必须有一个 +
符号。到目前为止介绍的重复构造中没有一种提供了一种以不同方式处理第一个元素的方法。
如果我们暂时忽略第一个数字,则其余输入是 "+ 13 + 3 + 21"
。这可以很容易地由 foldl
(或 repeated
)解析。
using plus_token = token<lit_c<'+'>>; using plus_int = last_of<plus_token, int_token>; using sum_parser2 = foldl<plus_int, int_<0>, sum_op>;
它使用 plus_int
,即 last_of
<plus_token, int_token>
,作为重复使用的解析器来获取数字。它的作用如下:
plus_token
来解析 +
符号以及可能跟随的任何空白。int_token
来解析数字。last_of
将上述两项结合起来,按顺序使用这两个解析器,并仅保留第二个解析器使用后的结果(解析 +
符号的结果被丢弃—我们不关心它)。这样,last_of
<plus_token, int_token>
返回数字的值作为解析结果,就像我们之前的解析器 int_token
一样。正因为如此,它可以用作上一个示例中 int_token
的直接替换,我们得到了用于更新语言的解析器。或者至少是除了第一个数字之外的所有数字。
这个 foldl
无法解析第一个元素,因为它期望在每个数字之前都有一个 +
符号。您可能会想到让上面的方法中的 +
符号可选—不要这样做。这使得解析器也接受 "11 + 13 3 21"
,因为 +
符号现在到处都是可选的。
您可以做的是使用 int_token
解析第一个元素,使用上面基于 foldl
的解决方案解析其余元素,并将两者的结果相加。这留给读者作为练习。
Metaparse 提供 foldl_start_with_parser
来实现此目的。foldl_start_with_parser
与 foldl
相同。区别在于,它不使用初始值来组合列表元素,而是接受一个初始解析器。
using plus_token = token<lit_c<'+'>>; using plus_int = last_of<plus_token, int_token>; using sum_parser3 = foldl_start_with_parser<plus_int, int_token, sum_op>;
foldl_start_with_parser
首先应用该初始解析器,并使用它返回的结果作为折叠的初始值。之后,它执行与 foldl
相同的操作。下图展示了如何使用它来解析以 +
符号分隔的数字列表:
如图所示,它使用 int_token
开始解析数字列表,使用其值作为折叠的起始值(早期方法使用 int_<0>
作为此起始值)。然后,它通过多次使用 plus_int
来解析列表的所有元素。
![]() |
注意 |
---|---|
注意,如果您是第一次阅读本手册,您可能想跳过本节,而尝试使用 |
foldl_start_with_parser
有其从右的配对,foldr_start_with_parser
。它使用与 foldl_start_with_parser
相同的元素,但顺序不同。下面是用 foldr_start_with_parser
实现我们示例语言的解析器:
using plus_token = token<lit_c<'+'>>; using int_plus = first_of<int_token, plus_token>; using sum_parser4 = foldr_start_with_parser<int_plus, int_token, sum_op>;
注意,它使用的是 int_plus
而不是 plus_int
。这是因为用于初始折叠值的解析器在 int_plus
尽可能多地解析输入后才应用。第一次听起来可能很奇怪,但下图应该有助于您理解它的工作原理:
您可以看到,它首先应用重复应用于输入的解析器,因此我们需要重复解析 int_token plus_token
而不是 plus_token int_token
。最后一个数字后面没有 +
,因此 int_plus
无法解析它,迭代就会停止。foldr_start_with_parser
然后使用另一个解析器 int_token
来解析输入。它会成功,它返回的结果将用作从右侧开始折叠的起始值。
![]() |
注意 |
---|---|
请注意,如上所述, |
使用 foldl_start_with_parser
构建的解析器可以解析正确的输入。然而,情况并非总是如此。例如,考虑以下输入:
BOOST_METAPARSE_STRING("11 + 13 + 3 + 21 +")
这是一个无效的表达式。然而,如果我们使用前面介绍的基于 foldl_start_with_parser
的解析器(sum_parser3
)来解析它,它会接受输入,结果是 48
。这是因为 foldl_start_with_parser
会尽可能地解析输入。它解析第一个 int_token
(11
),然后开始解析 plus_int
元素(+ 13
、+ 3
、+ 21
)。在解析完所有这些之后,它会尝试使用 plus_int
来解析剩余的 " +"
输入,这会导致失败,因此 foldl_start_with_parser
在 + 21
之后停止。
问题在于,解析器会解析从开始位置开始的最长子表达式,该子表达式代表一个有效表达式。其余部分被忽略。可以使用 entire_input
来包装解析器,以确保拒绝末尾带有无效额外字符的表达式,但这样并不能使错误消息有用。(entire_input
只能告诉无效表达式的作者,在 + 21
之后有问题。)
Metaparse 提供 foldl_reject_incomplete_start_with_parser
,它的功能与 foldl_start_with_parser
相同,不同之处在于,一旦找不到进一步的重复,它就会检查重复解析器(在本例中为 plus_int
)在哪里失败。当它能够取得任何进展时(例如,它找到一个 +
符号),foldl_reject_incomplete_start_with_parser
会假设表达式的作者打算让重复更长,但犯了一个错误,并传播来自最后一个损坏表达式的错误消息。
上图展示了 foldl_reject_incomplete_start_with_parser
如何解析示例无效输入及其失败情况。这可用于从解析器获得更好的错误报告。
其他折叠解析器也有其 f
版本。(例如 foldr_reject_incomplete
、foldl_reject_incomplete1
等)。
您可能已经注意到,有许多不同的折叠解析器组合子。为了帮助您找到合适的,使用了以下命名约定:
![]() |
注意 |
---|---|
请注意,没有 |
使用 Metaparse 构建的解析器是在编译时解析文本(或代码)的模板元程序。以下是解析的“结果”可以是什么的列表:
printf
格式字符串并返回预期参数的类型列表(例如 boost::mpl::vector
)的解析器。boost::xpressive::sregex
对象。请参阅 Metaparse 的 regex
示例。compile_to_native_code
示例。meta_hs
示例。Metaparse 提供了一种以类似 EBNF 的语法定义语法的方法。可以使用 grammar
模板来定义语法。它可以按如下方式使用:
grammar<BOOST_METAPARSE_STRING("plus_exp")> ::import<BOOST_METAPARSE_STRING("int_token"), token<int_>>::type ::rule<BOOST_METAPARSE_STRING("ws ::= (' ' | '\n' | '\r' | '\t')*")>::type ::rule<BOOST_METAPARSE_STRING("plus_token ::= '+' ws"), front<_1>>::type ::rule<BOOST_METAPARSE_STRING("plus_exp ::= int_token (plus_token int_token)*"), plus_action>::type
上面的代码从语法定义中定义了一个解析器。语法的起始符号是 plus_exp
。以 ::rule
开头的行定义了规则。规则可以选择性地具有语义动作,这是一个模板元函数类,它在应用规则后转换解析结果。可以通过导入现有解析器来将它们绑定到名称并在规则中使用。以 ::import
开头的行将现有解析器绑定到名称。
语法定义的結果是一个解析器,它可以提供给其他解析器组合子或直接使用。考虑到语法可以导入现有解析器并构建新解析器,因此它们本身也是解析器组合子。
Metaparse 基于模板元编程,但是,C++11 提供了 constexpr
,它也可以用于编译时解析。虽然基于 constexpr
实现解析器对于 C++ 开发人员来说更容易,因为它的语法类似于语言的常规语法,但解析结果必须是一个 constexpr
值。基于模板元编程的解析器可以构建类型作为解析结果。这些类型可以是装箱的 constexpr
值,但也可以是元函数类、具有可在运行时调用的静态函数的类等。
当使用 Metaparse 构建的解析器需要一个子解析器来处理输入文本的一部分并生成 constexpr
值作为解析结果时,可以使用基于 constexpr
函数的子解析器。Metaparse 可以与它们集成,并将它们的结果提升到 C++ 模板元编程中。其中一个示例可以在示例(constexpr_parser
)中找到。这种能力使得 Metaparse 可以与基于 constexpr
的解析库集成。
使用 Metaparse 可以为上下文无关文法编写解析器。然而,这并不是可以使用文法的最通用类别。由于 Metaparse 是一个高度可扩展的框架,因此不清楚 Metaparse 本身的极限是什么。例如,Metaparse 提供了 accept_when
解析器组合子。它可用于为启用/禁用特定规则提供任意谓词。您可以进一步提供整个文法的图灵机(作为元函数)作为谓词,因此您可以为可以使用图灵机解析的无限制文法构建解析器。请注意,这样的解析器不能被认为是使用 Metaparse 构建的解析器,但是,尚不清楚一个解决方案可能走多远仍然被认为是使用了 Metaparse。
Metaparse 假设解析器是确定性的,因为它们只有一个“结果”。当然,可以编写返回结果集(或列表或其他容器)作为该“一个”结果的解析器和组合子,但这可以被认为是构建了一个新的解析器库。Metaparse 没有明确的界限。
Metaparse 支持构建自顶向下解析器,并且不支持左递归,因为它会导致无限递归。支持右递归,但是,在大多数情况下,迭代解析器组合子提供了更好的选择。