Boost C++ 库

这是世界上最受推崇、设计最精巧的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu《C++ 编码标准》

用户手册 - Boost C++ 函数库
PrevUpHomeNext

有关解析器的解释,请参阅 参考 部分中的 解析器 部分。

解析器以 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 宏指定。

源位置使用编译时数据结构来描述。可以使用以下函数进行查询:

输入的开始是 start,它需要包含 <boost/metaparse/start.hpp>

错误使用编译时数据结构来描述。它包含有关检测到错误的位置以及一些关于错误的 描述。可以使用 debug_parsing_error 来显示错误消息。Metaparse 提供 BOOST_METAPARSE_DEFINE_ERROR 宏来定义简单的 解析错误消息

  • 一个不解析任何内容且总是成功的解析器是 return_
  • 一个总是失败的解析器是 fail
  • 一个解析单个字符并将其作为结果返回的解析器是 one_char

可以通过组合简单解析器来构建复杂的解析器。解析器库包含许多解析器组合子,它们可以根据现有解析器构建新解析器。

例如,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 的 foldaccumulate 来汇总类型列表中的数字。下面是一个例子:

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> 的结果)中的每个项。

[Note] 注意

请注意,transform 包装了另一个解析器 repeated<int_token>。它使用该解析器解析输入,获取解析结果并更改该结果。transform 本身将是一个返回更新结果的解析器。

这样做虽然有效,但效率不高:它有一个循环,逐个解析整数,构建一个类型列表,然后遍历这个类型列表来汇总结果。在应用程序中使用模板元程序可能会严重影响编译器的内存使用和编译速度,因此我建议谨慎处理这些事情。

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

[Note] 注意

注意,如果您是第一次阅读本手册,您可能想跳过本节,然后继续阅读 介绍 foldl_start_with_parser

您可能已经注意到 Metaparse 也提供了 foldrfoldlfoldr 之间的区别在于汇总结果的方向。(l 代表 从左r 代表 从右)下面是使用 foldr 实现 better_sum_parser 的图示:

如您所见,这与使用 foldl 非常相似,但来自 int_token 单独应用的汇总结果是按从右到左的顺序进行的。由于 sum_op 是加法,它不会影响最终结果,但在其他情况下可能会。

[Note] 注意

注意,foldl 的实现比 foldr 更高效。尽可能优先使用 foldl

正如您可能预料的那样,Metaparse 也提供 foldr1,它从右侧折叠并拒绝空输入。

让我们改变一下我们的小语言的语法。与其说是数字列表,不如说我们期望数字用 + 符号分隔。我们的示例输入变成如下:

BOOST_METAPARSE_STRING("11 + 13 + 3 + 21")

使用 foldlrepeated 解析它很困难:在每个元素除了第一个元素之前必须有一个 + 符号。到目前为止介绍的重复构造中没有一种提供了一种以不同方式处理第一个元素的方法。

如果我们暂时忽略第一个数字,则其余输入是 "+ 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_parserfoldl 相同。区别在于,它不使用初始值来组合列表元素,而是接受一个初始解析器

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 来解析列表的所有元素。

[Note] 注意

注意,如果您是第一次阅读本手册,您可能想跳过本节,而尝试使用 foldl_start_with_parser 创建一些解析器。

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 来解析输入。它会成功,它返回的结果将用作从右侧开始折叠的起始值。

[Note] 注意

请注意,如上所述,foldl_start_with_parser 的实现比 foldr_start_with_parser 更高效。尽可能优先使用 foldl_start_with_parser

使用 foldl_start_with_parser 构建的解析器可以解析正确的输入。然而,情况并非总是如此。例如,考虑以下输入:

BOOST_METAPARSE_STRING("11 + 13 + 3 + 21 +")

这是一个无效的表达式。然而,如果我们使用前面介绍的基于 foldl_start_with_parser 的解析器(sum_parser3)来解析它,它会接受输入,结果是 48。这是因为 foldl_start_with_parser尽可能地解析输入。它解析第一个 int_token11),然后开始解析 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_incompletefoldl_reject_incomplete1 等)。

您可能已经注意到,有许多不同的折叠解析器组合子。为了帮助您找到合适的,使用了以下命名约定:

[Note] 注意

请注意,没有 foldr_reject_incomplete_start_with_parser。右折叠解析器的 p 版本在特殊解析器之后应用该特殊解析器,其结果是初始值。因此,当解析单个重复元素的解析器失败时,foldr_start_with_parser 将应用该特殊的最终解析器,而不是检查重复元素的解析器是如何失败的。

使用 Metaparse 构建的解析器是在编译时解析文本(或代码)的模板元程序。以下是解析的“结果”可以是什么的列表:

  • 一个类型。例如,解析 printf 格式字符串并返回预期参数的类型列表(例如 boost::mpl::vector)的解析器。
  • 一个常量值。例如,计算器语言的结果。有关详细信息,请参阅 入门 部分。
  • 一个运行时对象。可以生成一个静态运行时对象,该对象可在运行时使用。例如,在编译时解析正则表达式并构建 boost::xpressive::sregex 对象。请参阅 Metaparse 的 regex 示例。
  • 一个 C++ 函数,可以在运行时调用。可以生成一个可以在运行时调用的 C++ 函数。这对于从 EDSL 生成本地(已优化)代码很有用。请参阅 Metaparse 的 compile_to_native_code 示例。
  • 一个模板元函数类。解析的结果可能是一个类型,而这个类型是一个模板元函数类。这对于构建模板元编程的 EDSL 很有用。请参阅 Metaparse 的 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 支持构建自顶向下解析器,并且不支持左递归,因为它会导致无限递归。支持右递归,但是,在大多数情况下,迭代解析器组合子提供了更好的选择。


PrevUpHomeNext