Boost C++ 库

one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

教程 - Boost C++ 函数库
PrevUpHomeNext

首先,让我们介绍一下将在整个文档中使用的术语。

一个 语义动作 是一个与解析器关联的任意逻辑片段,它仅在解析器匹配时执行。

更简单的解析器可以组合成更复杂的解析器。给定一个组合操作 C,以及解析器 P0P1、... PNC(P0, P1, ... PN) 创建一个新的解析器 Q。这创建了一个 解析树QP1 的父节点,P2Q 的子节点,依此类推。解析器按照此拓扑结构隐含的从上到下的方式进行应用。当您使用 Q 解析字符串时,它将使用 P0P1 等来执行实际工作。如果 P3 正在用于解析输入,这意味着 Q 也在这样做,因为 Q 的解析方式是通过分派到其子项来执行部分或全部工作。在解析的任何一点,将只有一个没有子项的解析器被用于解析输入;所有其他正在使用的解析器都是解析树中的祖先。

一个 子解析器 是另一个解析器的子解析器。

顶级解析器 是解析器树的根。

当前解析器最底层解析器 是当前正在用于解析输入的、没有子项的解析器。

一个 规则 是一种解析器,它使构建大型、复杂的解析器变得更容易。一个 子规则 是一个属于其他规则的规则。 当前规则最底层规则 是当前用于解析输入且没有子规则的那个规则。请注意,虽然总是只有一个当前解析器,但可能有一个当前规则,也可能没有——规则是一种解析器,您在解析的特定点可能正在使用它,也可能没有。

顶级解析 是由顶级解析器执行的解析操作。这个术语是必要的,因为虽然大多数解析失败都是局部的,但有些解析失败会导致 parse() 调用指示整个解析失败。在这些情况下,我们说这种局部失败“导致顶级解析失败”。

在整个 Boost.Parser 文档中,我将引用“调用 parse()”。将其读作“调用 The parse() API 中描述的任何一个函数”。这包括 prefix_parse()callback_parse()callback_prefix_parse()

文档中经常出现几种特殊的解析器。

一种是 序列解析器;您会看到它使用 operator>> 创建,如 p1 >> p2 >> p3 所示。序列解析器尝试按顺序匹配其所有子解析器到输入。只有当其所有子解析器都匹配时,它才匹配输入。

另一种是 选择解析器;您会看到它使用 operator| 创建,如 p1 | p2 | p3 所示。选择解析器尝试按顺序匹配其所有子解析器到输入;它最多匹配一个子解析器后停止。只有当其一个子解析器匹配时,它才匹配输入。

最后,还有一个 排列解析器;它使用 operator|| 创建,如 p1 || p2 || p3 所示。排列解析器尝试按任意顺序匹配其所有子解析器到输入。所以解析器 p1 || p2 || p3 等价于 (p1 >> p2 >> p3) | (p1 >> p3 >> p2) | (p2 >> p1 >> p3) | (p2 >> p3 >> p1) | (p3 >> p1 >> p2) | (p3 >> p2 >> p1)。希望它的简洁性优势不言而喻。它匹配输入当且仅当其所有子解析器都匹配,无论它们的匹配顺序如何。

Boost.Parser 解析器都有一个与之关联的 属性,或者明确地没有属性。属性是解析器在匹配输入时生成的值。例如,解析器 double_ 在匹配输入时生成一个 doubleATTR() 是一个概念上的宏,它会展开为传递给它的解析器的属性类型; ATTR(double_)double。这类似于 attribute 类型特征。

接下来,我们将研究一些使用 Boost.Parser 进行解析的简单程序。我们将从小处着手,然后逐步深入。

这是几乎可以用尽的最精简的 Boost.Parser 使用示例。我们从命令行获取一个字符串,如果没有则默认为“World”,然后对其进行解析。

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main(int argc, char const * argv[])
{
    std::string input = "World";
    if (1 < argc)
        input = argv[1];

    std::string result;
    bp::parse(input, *bp::char_, result);
    std::cout << "Hello, " << result << "!\n";
}

表达式 *bp::char_ 是一个解析器表达式。它使用了 Boost.Parser 提供的众多解析器之一:char_。与所有 Boost.Parser 解析器一样,它具有某些已定义的运算符。在这种情况下,*bp::char_ 使用了重载的 operator* 作为 Kleene star 运算符的 C++ 版本。由于 C++ 没有后缀一元 * 运算符,我们必须使用我们拥有的那个,所以它被用作前缀。

所以,*bp::char_ 意味着“任意数量的字符”。换句话说,它实际上不会失败。即使是空字符串也会匹配它。

解析操作是通过调用 parse() 函数来执行的,将解析器作为参数之一传递。

bp::parse(input, *bp::char_, result);

这里的参数是:input,要解析的范围;*bp::char_,用于执行解析的解析器;以及 result,一个输出参数,用于存放解析结果。不要过分纠结于这种从 parse() 中获取解析结果的方法;有多种方法可以做到这一点,我们将在后续章节中全部介绍。

另外,暂时忽略 Boost.Parser 如何推断出 *bp::char_ 解析器的结果类型是 std::string。这背后有明确的规则,我们稍后会讨论。

这次 parse() 调用的效果并不有趣——由于我们传入的解析器永远不会失败,而且我们正在将输出放置在与输入相同的类型中,它只是将 input 的内容复制到 result

让我们来看一个稍微复杂一些的例子,即使它仍然很简单。我们不接受任意字符,而是要求一定的结构。我们解析一个或多个 double,以逗号分隔。

Boost.Parser 中表示 double 的解析器是 double_。所以,要解析一个 double,我们只需使用它。如果我们想连续解析两个 double,我们会这样做:

boost::parser::double_ >> boost::parser::double_

此表达式中的 operator>> 是序列运算符;读作“后面跟着”。如果我们结合序列运算符和 Kleene star,我们可以通过编写来获得我们想要的解析器:

boost::parser::double_ >> *(',' >> boost::parser::double_)

这是一个匹配至少一个 double 的解析器——因为上面表达式中的第一个 double_——后面跟着零个或多个“逗号后面跟着一个 double”的实例。请注意,我们可以直接使用 ','。虽然它不是一个解析器,但 operator>> 和 Boost.Parser 解析器上定义的其他运算符接受字符/解析器对参数;这些运算符重载将创建正确的解析器来识别 ','

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main()
{
    std::cout << "Enter a list of doubles, separated by commas.  No pressure. ";
    std::string input;
    std::getline(std::cin, input);

    auto const result = bp::parse(input, bp::double_ >> *(',' >> bp::double_));

    if (result) {
        std::cout << "Great! It looks like you entered:\n";
        for (double x : *result) {
            std::cout << x << "\n";
        }
    } else {
        std::cout
            << "Good job!  Please proceed to the recovery annex for cake.\n";
    }
}

第一个示例填充了一个输出参数来传递解析结果。这个 parse() 调用返回一个结果。正如您所见,该结果在上下文中可转换为 bool,而 *result 是某种范围。事实上,这次 parse() 调用的返回类型是 std::optional<std::vector<double>>。当然,如果解析失败,将返回 std::nullopt。我们稍后会看 Boost.Parser 如何将解析器的类型映射到返回类型或填充的输出参数的类型。

[Note] 注意

有一个类型特征可以告诉您解析器的属性类型,即 attribute(以及相关的别名 attribute_t)。我们将在“属性生成”部分更详细地讨论它。

如果在 shell 中运行它,结果是这样的:

$ example/trivial
Enter a list of doubles, separated by commas.  No pressure. 5.6,8.9
Great! It looks like you entered:
5.6
8.9
$ example/trivial
Enter a list of doubles, separated by commas.  No pressure. 5.6, 8.9
Good job!  Please proceed to the recovery annex for cake.

它不识别 "5.6, 8.9"。这是因为期望一个紧跟着 立即 一个 double 的逗号,但我却在逗号后面加了一个空格。如果我在逗号前面,或者在 double 列表的前后加空格,也会发生同样的解析失败。

还有一点:有一个更好的方法来编写上面的解析器。与其重复 double_ 子解析器,我们本可以这样写:

bp::double_ % ','

这在语义上等同于 bp::double_ >> *(',' >> bp::double_)。这种模式——某个输入部分重复一次或多次,每次之间都有一个分隔符——非常常见,以至于有一个专门的操作符 operator%。从现在开始,我们将使用该运算符。

让我们修改一下刚才看到的简单解析器,使其能够忽略它在 doubles 和逗号之间可能找到的任何空格。为了在找到空格时跳过它,我们可以将一个 跳过解析器 传递给我们的 parse() 调用(我们不需要更改传递给 parse() 的解析器)。在这里,我们使用 ws,它匹配任何 Unicode 空白字符。

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main()
{
    std::cout << "Enter a list of doubles, separated by commas.  No pressure. ";
    std::string input;
    std::getline(std::cin, input);

    auto const result = bp::parse(input, bp::double_ % ',', bp::ws);

    if (result) {
        std::cout << "Great! It looks like you entered:\n";
        for (double x : *result) {
            std::cout << x << "\n";
        }
    } else {
        std::cout
            << "Good job!  Please proceed to the recovery annex for cake.\n";
    }
}

跳过解析器,或 skipper,在 parse() 调用传入的解析器中的子解析器之间运行。在此示例中,skipper 在解析第一个 double 之前运行,在解析任何后续逗号或 double 之前运行,并在最后运行。因此,字符串 "3.6,5.9"" 3.6 , \t 5.9 " 被此程序解析为相同。

跳过是 Boost.Parser 中的一个重要概念。您可以跳过任何内容,而不仅仅是空白;还有许多其他您可能想跳过的内容。传递给 parse() 的 skipper 可以是任意解析器。例如,如果您编写了一个脚本语言的解析器,您可以编写一个 skipper 来跳过空格、行内注释和行尾注释。

在文档的其余部分,我们将几乎专门使用跳过解析器。忽略输入中您不关心的部分的能力非常方便,以至于实际上很少会进行不跳过的解析。

与所有解析系统(lex & yacc、Boost.Spirit 等)一样,Boost.Parser 提供了一种机制,用于将语义动作与解析的不同部分关联起来。这是一个几乎与上一个示例相同的程序,不同之处在于它基于一个语义动作来实现,该动作将每个解析出的 double 追加到结果中,而不是自动构建并返回结果。要做到这一点,我们将上一个示例中的 double_ 替换为 double_[action]action 是我们的语义动作。

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main()
{
    std::cout << "Enter a list of doubles, separated by commas. ";
    std::string input;
    std::getline(std::cin, input);

    std::vector<double> result;
    auto const action = [&result](auto & ctx) {
        std::cout << "Got one!\n";
        result.push_back(_attr(ctx));
    };
    auto const action_parser = bp::double_[action];
    auto const success = bp::parse(input, action_parser % ',', bp::ws);

    if (success) {
        std::cout << "You entered:\n";
        for (double x : result) {
            std::cout << x << "\n";
        }
    } else {
        std::cout << "Parse failure.\n";
    }
}

在 shell 中运行,它看起来是这样的:

$ example/semantic_actions
Enter a list of doubles, separated by commas. 4,3
Got one!
Got one!
You entered:
4
3

在 Boost.Parser 中,语义动作是通过可调用对象实现的,这些对象接受一个解析上下文对象作为参数。解析上下文对象表示解析的当前状态。在示例中,我们使用了这个 lambda 作为我们的可调用对象:

auto const action = [&result](auto & ctx) {
    std::cout << "Got one!\n";
    result.push_back(_attr(ctx));
};

我们既向 std::cout 打印消息,又在 lambda 中记录了解析结果。如果您愿意,它可以执行所有这些操作,其中任何一项,或都不执行。我们通过询问解析上下文来获取 lambda 中的已解析 double_attr(ctx) 是如何从解析上下文获取与语义动作关联的解析器产生的属性。有许多函数,如 _attr(),可用于访问解析上下文中的状态。我们将在稍后介绍更多内容。The Parse Context 定义了解析上下文的确切内容及其工作方式。

请注意,您不能直接将未修饰的 lambda 写入语义动作。否则,编译器会看到两个 '[' 字符,并认为它即将解析一个属性。括号可以解决这个问题:

p[([](auto & ctx){/*...*/})]

在执行此操作之前,请注意,您作为语义动作编写的 lambda 几乎总是通用的(具有 auto & ctx 参数),因此它们非常可重用。您编写的大多数语义动作 lambda 都应该在行外编写,并给予一个好的名称。即使它们不被重用,命名 lambda 也能使您的解析器更小、更易读。

[Important] 重要提示

将语义动作附加到解析器会移除其属性。也就是说,ATTR(p[a]) 始终是特殊的无属性类型 none,而不管 ATTR(p) 的类型是什么。

规则内部的语义动作

当在 rules 中使用语义动作时,还有其他一些形式。请参阅 More About Rules 获取详细信息。

到目前为止,我们已经看到了一些解析文本并生成相关属性的示例。有时,您想找到输入中的某个子范围,其中包含您正在寻找的内容,并且您不想生成任何属性。

有两个 指令 会影响任何解析器的属性类型:raw[]string_view[]。(我们将在后面的“指令”部分更详细地介绍指令。目前,您只需要知道指令包装了解析器,并改变了它的某些功能方面。)

raw[]

raw[] 将其解析器的属性更改为 subrange,其 begin()end() 返回与 p 匹配的被解析序列的边界。

namespace bp = boost::parser;
auto int_parser = bp::int_ % ',';            // ATTR(int_parser) is std::vector<int>
auto subrange_parser = bp::raw[int_parser];  // ATTR(subrange_parser) is a subrange

// Parse using int_parser, generating integers.
auto ints = bp::parse("1, 2, 3, 4", int_parser, bp::ws);
assert(ints);
assert(*ints == std::vector<int>({1, 2, 3, 4}));

// Parse again using int_parser, but this time generating only the
// subrange matched by int_parser.  (prefix_parse() allows matches that
// don't consume the entire input.)
auto const str = std::string("1, 2, 3, 4, a, b, c");
auto first = str.begin();
auto range = bp::prefix_parse(first, str.end(), subrange_parser, bp::ws);
assert(range);
assert(range->begin() == str.begin());
assert(range->end() == str.begin() + 10);

static_assert(std::is_same_v<
              decltype(range),
              std::optional<bp::subrange<std::string::const_iterator>>>);

请注意,subrange 的迭代器类型是 std::string::const_iterator,因为这是传递给 prefix_parse() 的迭代器类型。如果我们向 prefix_parse() 传递了 char const * 迭代器,那将是迭代器类型。唯一的例外来自 Unicode 感知解析(请参阅“Unicode 支持”)。在其中一些情况下,解析中使用的迭代器不是您传递的那个。例如,如果您使用 char8_t * 迭代器调用 prefix_parse(),它将创建一个 UTF-8 到 UTF-32 的转码视图,并解析该视图的迭代器。在这种情况下,您将获得一个 subrange,其迭代器类型是转码迭代器。当这种情况发生时,您可以通过调用返回的 subrange 中的每个转码迭代器上的 .base() 成员函数来获取底层迭代器——您传递给 prefix_parse() 的那个。

auto const u8str = std::u8string(u8"1, 2, 3, 4, a, b, c");
auto u8first = u8str.begin();
auto u8range = bp::prefix_parse(u8first, u8str.end(), subrange_parser, bp::ws);
assert(u8range);
assert(u8range->begin().base() == u8str.begin());
assert(u8range->end().base() == u8str.begin() + 10);
string_view[]

string_view[] 的语义与 raw[] 非常相似,只是它产生的是 std::basic_string_view<CharT>(其中 CharT 是被解析的底层范围的类型),而不是 subrange。为了实现这一点,底层范围必须是连续的。迭代器的连续性在 C++20 之前是无法检测的,因此此指令仅在 C++20 及更高版本中可用。

namespace bp = boost::parser;
auto int_parser = bp::int_ % ',';              // ATTR(int_parser) is std::vector<int>
auto sv_parser = bp::string_view[int_parser];  // ATTR(sv_parser) is a string_view

auto const str = std::string("1, 2, 3, 4, a, b, c");
auto first = str.begin();
auto sv1 = bp::prefix_parse(first, str.end(), sv_parser, bp::ws);
assert(sv1);
assert(*sv1 == str.substr(0, 10));

static_assert(std::is_same_v<decltype(sv1), std::optional<std::string_view>>);

由于 string_view[] 产生 string_view,它不能像上面为 raw[] 描述的那样返回转码迭代器。如果您使用 string_view[] 解析 CharT 序列,您将获得一个 std::basic_string_view<CharT>。如果解析在使用 Unicode 感知路径中的转码,string_view[] 将在必要时分解转码迭代器。如果您将转码视图传递给 parse() 或将转码迭代器传递给 prefix_parse()string_view[] 仍能毫无问题地看到转码迭代器,并为您提供底层范围一部分的 string_view

auto sv2 = bp::parse("1, 2, 3, 4" | bp::as_utf32, sv_parser, bp::ws);
assert(sv2);
assert(*sv2 == "1, 2, 3, 4");

static_assert(std::is_same_v<decltype(sv2), std::optional<std::string_view>>);

现在是时候详细描述解析上下文了。您编写的任何语义动作都需要使用解析上下文中的状态,因此您需要了解可用的内容。

解析上下文是一个对象,它存储解析的当前状态——当前迭代器和结束迭代器、错误处理程序等。在解析的不同时间,数据似乎被“添加到”它或“从中删除”。例如,当一个具有语义动作 a 的解析器 p 成功时,上下文会将 p 生成的属性添加到解析上下文中,然后调用 a,并将上下文传递给它。

尽管上下文对象似乎有东西被添加或删除,但事实并非如此。实际上,没有一个单独的上下文对象。上下文在解析期间的各种时间形成,通常是在开始子解析器时。每个上下文都是通过获取前一个上下文并根据需要添加或更改成员来形成新的上下文对象而创建的。当包含新上下文对象的函数返回时,其上下文对象(如果有)将被销毁。这样做效率很高,因为解析上下文只有大约十几个数据成员,每个数据成员的大小都不大于指针。因此,在修改上下文时复制整个上下文是快速的。上下文不进行内存分配。

[Tip] 提示

所有这些以解析上下文作为第一个参数的函数都可以通过依赖注入(Argument-Dependent Lookup)找到。您可能永远不需要用 boost::parser:: 来限定它们。

始终可用的数据的访问器

按照惯例,所有接受解析上下文并且因此 intended for use inside semantic actions 的 Boost.Parser 函数的名称都以一个下划线开头。

_pass()

_pass() 返回一个对 bool 的引用,该引用指示当前解析的成功或失败。这可以用来强制当前解析通过或失败。

[](auto & ctx) {
    // If the attribute fails to meet this predicate, fail the parse.
    if (!necessary_condition(_attr(ctx)))
        _pass(ctx) = false;
}

请注意,要执行语义动作,其关联的解析器必须已经成功。所以,除非您之前在动作中编写了 _pass(ctx) = false,否则 _pass(ctx) = true 不会起任何作用;它是多余的。

_begin(), _end() and _where()

_begin()_end() 分别返回您传递给 parse() 的范围的开始和结束。 _where() 返回一个 subrange,指示当前解析匹配的输入的边界。如果您只想解析某些文本并返回一个由某些元素位置组成的结果,而不产生任何其他属性,则 _where() 会很有用。 _where() 在跟踪元素位置以在解析的后续阶段提供良好的诊断信息方面也至关重要。想象一下 XML 中的标签不匹配;如果您在元素末尾解析了闭合标签,并且它与开始标签不匹配,您希望生成一条提及或显示两个标签的错误消息。将 _where(ctx).begin() 存储在闭合标签解析器可用的地方,将有助于此。有关此示例,请参阅“错误处理和调试”。

_error_handler()

_error_handler() 返回一个对传递给 parse() 的解析器关联的错误处理程序的引用。使用 _error_handler(),您可以在语义动作中生成错误和警告。有关具体示例,请参阅“错误处理和调试”。

有时才可用的数据的访问器
_attr()

_attr() 返回当前解析器属性值的引用。仅当当前解析器成功解析时才可用。如果解析器没有语义动作,则不会将属性添加到解析上下文中。它可用于读取和写入当前解析器的属性。

[](auto & ctx) { _attr(ctx) = 3; }

如果当前解析器没有属性,则返回 none

_val()

_val() 返回当前用于解析的规则的属性值的引用(如果存在),即使在规则解析成功之前也可用。它可以用于设置当前规则的属性,即使是从规则内的子解析器。假设我们正在编写一个具有语义动作的解析器,该语义动作位于一个规则内。如果我们想将当前规则的值设置为子解析器属性的某个函数,我们将这样编写语义动作:

[](auto & ctx) { _val(ctx) = some_function(_attr(ctx)); }

如果没有当前规则,或者当前规则没有属性,则返回 none

您需要在 rule 解析器的默认属性类型与 rule 的属性类型不直接兼容的情况下使用 _val()。在这些情况下,您需要编写像上面的示例那样的代码来从规则解析器的已生成属性计算规则的属性。有关 rules 的更多信息,请参阅下一页和“更多关于规则”。

_globals()

_globals() 返回一个对用户提供的对象的引用,该对象包含您想在解析过程中使用的任何数据。“parse 的全局变量”是一个对象——通常是一个结构体——您将其提供给顶级解析器。然后,您可以在解析过程中的任何时候使用 _globals() 来访问它。我们将在“The parse() API”中看到全局变量如何与顶级解析器相关联。例如,假设您的解析有一个早期部分需要记录一些黑名单值,并且解析的后续部分可能需要解析值,如果看到黑名单值则解析失败。在解析的早期部分,您可以编写如下代码。

[](auto & ctx) {
    // black_list is a std::unordered_set.
    _globals(ctx).black_list.insert(_attr(ctx));
}

然后在解析的后期,您可以使用 black_list 来检查解析的值。

[](auto & ctx) {
    if (_globals(ctx).black_list.contains(_attr(ctx)))
        _pass(ctx) = false;
}
_locals()

_locals() 返回当前正在解析的规则的局部值(如果存在)的引用。如果有两个或更多个局部值,_locals() 返回一个 boost::parser::tuple 的引用。具有局部变量的规则是我们尚未涉及的内容(请参阅“更多关于规则”),但目前您只需要知道您可以为 rule 提供模板参数(LocalState),并且规则将默认构造该类型的一个对象供规则内部使用。您可以通过 _locals() 访问它。

[](auto & ctx) {
    auto & local = _locals(ctx);
    // Use local here.  If 'local' is a hana::tuple, access its members like this:
    using namespace hana::literals;
    auto & first_element = local[0_c];
    auto & second_element = local[1_c];
}

如果没有当前规则,或者当前规则没有局部变量,则返回 none

_params()

_params(),与 _locals() 一样,适用于当前用于解析的规则(如果存在)(请参阅“更多关于规则”)。如果当前规则只有一个参数,它将返回该参数的引用;如果当前规则有多个参数,它将返回多个值的 boost::parser::tuple 的引用。如果没有当前规则,或者当前规则没有参数,则返回 none

_locals() 不同,您 rule 提供模板参数。而是调用 rulewith() 成员函数(再次参阅“更多关于规则”)。

[Note] 注意

none 是 Boost.Parser 中用于解析上下文访问器的返回值类型。 none 可转换为任何具有默认构造函数的类型,可从任何类型转换,可从任何类型赋值,并且所有可重载运算符都有模板重载。目的是,对 _val()_globals() 等的误用应该能够编译,并在运行时产生断言。经验表明,使用调试器来调查导致错误的堆栈比筛选编译器诊断信息是更好的用户体验。有关更详细的解释,请参阅“原理”部分。

_no_case()

_no_case() 如果当前解析上下文位于一个或多个(可能嵌套的)no_case[] 指令内部,则返回 true。我没有使用场景,但如果我不公开它,它将是上下文中唯一一个您无法从语义动作内部检查的内容。添加它很容易,所以我添加了。

这个例子与我们到目前为止看到的其他例子非常相似。它之所以不同,仅仅是因为它使用了一个 rule。作为类比,您可以将 char_double_ 这样的解析器想象成一行代码,而 rule 就像一个函数。像函数一样,rule 有自己的名称,甚至可以前向声明。下面是我们如何定义一个 rule,这类似于前向声明一个函数:

bp::rule<struct doubles, std::vector<double>> doubles = "doubles";

这声明了规则本身。 rule 是一个解析器,我们可以立即在其他解析器中使用它。该定义相当密集;请注意以下几点:

  • 第一个模板参数是标签类型 struct doubles。在这里,我们声明了标签类型并一次性使用了它;您也可以使用先前声明的标签类型。
  • 第二个模板参数是解析器的属性类型。如果您不提供此参数,则规则将没有属性。
  • 此规则对象本身称为 doubles
  • 我们为 doubles 提供了诊断文本 "doubles",以便 Boost.Parser 在调试期间生成解析器跟踪时知道如何引用它。

好的,既然 doubles 是一个解析器,它做什么呢?我们通过定义一个现在应该看起来很熟悉的单独的解析器来定义规则的行为:

auto const doubles_def = bp::double_ % ',';

这类似于为前向声明的函数编写定义。请注意,我们使用了名称 doubles_def。现在,doubles 规则解析器和 doubles_def 非规则解析器之间没有联系。这是故意的——我们希望能够单独定义它们。为了将它们连接起来,我们声明了 Boost.Parser 能够理解的接口的函数,并使用标签类型 struct doubles 来将它们连接在一起。我们为此使用了一个宏:

BOOST_PARSER_DEFINE_RULES(doubles);

此宏展开为使规则 doubles 和其解析器 doubles_def 协同工作的必要代码。 _def 后缀是此宏依赖于其工作的命名约定。标签类型允许规则解析器 doubles 在用作解析器时调用这些重载之一。

BOOST_PARSER_DEFINE_RULES 展开为两个名为 parse_rule() 的函数重载。在上面的例子中,每个重载都接受一个 struct doubles 参数(以区分它们与其他规则的 parse_rule() 重载)并使用 doubles_def 进行解析。您永远不需要自己调用 parse_rule() 的任何重载;它由实现 rules 的解析器 rule_parser 内部使用。

下面是为每个规则展开的宏的定义:

#define BOOST_PARSER_DEFINE_IMPL(_, rule_name_)                                \
    template<                                                                  \
        typename Iter,                                                         \
        typename Sentinel,                                                     \
        typename Context,                                                      \
        typename SkipParser>                                                   \
    decltype(rule_name_)::parser_type::attr_type parse_rule(                   \
        decltype(rule_name_)::parser_type::tag_type *,                         \
        Iter & first,                                                          \
        Sentinel last,                                                         \
        Context const & context,                                               \
        SkipParser const & skip,                                               \
        boost::parser::detail::flags flags,                                    \
        bool & success,                                                        \
        bool & dont_assign)                                                    \
    {                                                                          \
        auto const & parser = BOOST_PARSER_PP_CAT(rule_name_, _def);           \
        using attr_t =                                                         \
            decltype(parser(first, last, context, skip, flags, success));      \
        using attr_type = decltype(rule_name_)::parser_type::attr_type;        \
        if constexpr (boost::parser::detail::is_nope_v<attr_t>) {              \
            dont_assign = true;                                                \
            parser(first, last, context, skip, flags, success);                \
            return {};                                                         \
        } else if constexpr (std::is_same_v<attr_type, attr_t>) {              \
            return parser(first, last, context, skip, flags, success);         \
        } else if constexpr (std::is_constructible_v<attr_type, attr_t>) {     \
            return attr_type(                                                  \
                parser(first, last, context, skip, flags, success));           \
        } else {                                                               \
            attr_type attr{};                                                  \
            parser(first, last, context, skip, flags, success, attr);          \
            return attr;                                                       \
        }                                                                      \
    }                                                                          \
                                                                               \
    template<                                                                  \
        typename Iter,                                                         \
        typename Sentinel,                                                     \
        typename Context,                                                      \
        typename SkipParser,                                                   \
        typename Attribute>                                                    \
    void parse_rule(                                                           \
        decltype(rule_name_)::parser_type::tag_type *,                         \
        Iter & first,                                                          \
        Sentinel last,                                                         \
        Context const & context,                                               \
        SkipParser const & skip,                                               \
        boost::parser::detail::flags flags,                                    \
        bool & success,                                                        \
        bool & /*dont_assign*/,                                                \
        Attribute & retval)                                                    \
    {                                                                          \
        auto const & parser = BOOST_PARSER_PP_CAT(rule_name_, _def);           \
        using attr_t =                                                         \
            decltype(parser(first, last, context, skip, flags, success));      \
        if constexpr (boost::parser::detail::is_nope_v<attr_t>) {              \
            parser(first, last, context, skip, flags, success);                \
        } else {                                                               \
            parser(first, last, context, skip, flags, success, retval);        \
        }                                                                      \
    }

现在我们有了 doubles 解析器,我们可以像使用任何其他解析器一样使用它:

auto const result = bp::parse(input, doubles, bp::ws);

完整的程序:

#include <boost/parser/parser.hpp>

#include <deque>
#include <iostream>
#include <string>


namespace bp = boost::parser;


bp::rule<struct doubles, std::vector<double>> doubles = "doubles";
auto const doubles_def = bp::double_ % ',';
BOOST_PARSER_DEFINE_RULES(doubles);

int main()
{
    std::cout << "Please enter a list of doubles, separated by commas. ";
    std::string input;
    std::getline(std::cin, input);

    auto const result = bp::parse(input, doubles, bp::ws);

    if (result) {
        std::cout << "You entered:\n";
        for (double x : *result) {
            std::cout << x << "\n";
        }
    } else {
        std::cout << "Parse failure.\n";
    }
}

这一切都是为了引入 rules 的概念。您可能仍然不太清楚为什么您要使用 rulesrules 的用例和大量细节在后面的章节“更多关于规则”中。

[Note] 注意

rules 的存在意味着您可能永远不必编写低级解析器。您只需将现有的解析器组合到 rules 中即可。

到目前为止,我们只看到了简单的解析器,它们会重复解析相同的值(无论是否带有逗号和空格)。解析一系列值也很常见。假设您想解析一个员工记录。这是一个您可能会编写的解析器:

namespace bp = boost::parser;
auto employee_parser = bp::lit("employee")
    >> '{'
    >> bp::int_ >> ','
    >> quoted_string >> ','
    >> quoted_string >> ','
    >> bp::double_
    >> '}';

employee_parser 的属性类型是 boost::parser::tuple<int, std::string, std::string, double>。这很好,因为您在无需编写任何语义动作的情况下就获得了记录的所有解析数据。但现在您必须通过索引使用 get() 来获取所有单独的元素,这不太好。直接解析到您的程序将使用的最终数据结构会更好。这通常是某种 structclass。Boost.Parser 支持解析到任意聚合 struct,以及从给定元组可构造的非聚合。

聚合类型作为属性

如果我们有一个 struct,其数据成员的类型与 employee_parserboost::parser::tuple 属性类型中列出的类型相同,那么直接解析到它而不是先解析到元组然后再构造我们的 struct 会更好。幸运的是,这在 Boost.Parser 中直接可用。下面是一个直接解析到兼容的聚合类型的示例。

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


struct employee
{
    int age;
    std::string surname;
    std::string forename;
    double salary;
};

namespace bp = boost::parser;

int main()
{
    std::cout << "Enter employee record. ";
    std::string input;
    std::getline(std::cin, input);

    auto quoted_string = bp::lexeme['"' >> +(bp::char_ - '"') >> '"'];
    auto employee_p = bp::lit("employee")
        >> '{'
        >> bp::int_ >> ','
        >> quoted_string >> ','
        >> quoted_string >> ','
        >> bp::double_
        >> '}';

    employee record;
    auto const result = bp::parse(input, employee_p, bp::ws, record);

    if (result) {
        std::cout << "You entered:\nage:      " << record.age
                  << "\nsurname:  " << record.surname
                  << "\nforename: " << record.forename
                  << "\nsalary  : " << record.salary << "\n";
    } else {
        std::cout << "Parse failure.\n";
    }
}

不幸的是,这利用了宽松的属性赋值逻辑; employee_parser 解析器仍然有一个 boost::parser::tuple 属性。有关属性输出参数兼容性的说明,请参阅“The parse() API”。

因此,更常见的是希望创建一个返回特定类型(如 employee)的规则。仅仅通过给规则一个 struct 类型,我们就确保了这个解析器始终生成一个 employee 结构作为其属性,无论它在解析中的哪个位置。如果我们创建一个简单的解析器 P,它使用 employee_p 规则,例如 bp::int >> employee_p,那么 P 的属性类型将是 boost::parser::tuple<int, employee>

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


struct employee
{
    int age;
    std::string surname;
    std::string forename;
    double salary;
};

namespace bp = boost::parser;

bp::rule<struct quoted_string, std::string> quoted_string = "quoted name";
bp::rule<struct employee_p, employee> employee_p = "employee";

auto quoted_string_def = bp::lexeme['"' >> +(bp::char_ - '"') >> '"'];
auto employee_p_def = bp::lit("employee")
    >> '{'
    >> bp::int_ >> ','
    >> quoted_string >> ','
    >> quoted_string >> ','
    >> bp::double_
    >> '}';

BOOST_PARSER_DEFINE_RULES(quoted_string, employee_p);

int main()
{
    std::cout << "Enter employee record. ";
    std::string input;
    std::getline(std::cin, input);

    static_assert(std::is_aggregate_v<std::decay_t<employee &>>);

    auto const result = bp::parse(input, employee_p, bp::ws);

    if (result) {
        std::cout << "You entered:\nage:      " << result->age
                  << "\nsurname:  " << result->surname
                  << "\nforename: " << result->forename
                  << "\nsalary  : " << result->salary << "\n";
    } else {
        std::cout << "Parse failure.\n";
    }
}

就像您可以将一个 struct 作为输出参数传递给 parse()(当解析器的属性类型是元组时)一样,您也可以将一个元组作为输出参数传递给 parse()(当解析器的属性类型是结构体时)。

// Using the employee_p rule from above, with attribute type employee...
boost::parser::tuple<int, std::string, std::string, double> tup;
auto const result = bp::parse(input, employee_p, bp::ws, tup); // Ok!
[Important] 重要提示

structs 的这种自动使用,如同它们是元组一样,依赖于一些元编程。由于编译器限制,检测 struct 的数据成员数量的元程序被限制在最大成员数。幸运的是,这个限制是可配置的;请参阅 BOOST_PARSER_MAX_AGGREGATE_SIZE

通用 class 类型作为属性

很多时候,您没有一个想要从中生成的聚合结构。如果 Boost.Parser 能够检测到作为属性生成的元组的成员可以作为某个类型的构造函数的参数,那会比上面的聚合代码更好。所以,Boost.Parser 做到了这一点。

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main()
{
    std::cout << "Enter a string followed by two unsigned integers. ";
    std::string input;
    std::getline(std::cin, input);

    constexpr auto string_uint_uint =
        bp::lexeme[+(bp::char_ - ' ')] >> bp::uint_ >> bp::uint_;
    std::string string_from_parse;
    if (parse(input, string_uint_uint, bp::ws, string_from_parse))
        std::cout << "That yields this string: " << string_from_parse << "\n";
    else
        std::cout << "Parse failure.\n";

    std::cout << "Enter an unsigned integer followed by a string. ";
    std::getline(std::cin, input);
    std::cout << input << "\n";

    constexpr auto uint_string = bp::uint_ >> +bp::char_;
    std::vector<std::string> vector_from_parse;
    if (parse(input, uint_string, bp::ws, vector_from_parse)) {
        std::cout << "That yields this vector of strings:\n";
        for (auto && str : vector_from_parse) {
            std::cout << "  '" << str << "'\n";
        }
    } else {
        std::cout << "Parse failure.\n";
    }
}

让我们看看第一个解析。

constexpr auto string_uint_uint =
    bp::lexeme[+(bp::char_ - ' ')] >> bp::uint_ >> bp::uint_;
std::string string_from_parse;
if (parse(input, string_uint_uint, bp::ws, string_from_parse))
    std::cout << "That yields this string: " << string_from_parse << "\n";
else
    std::cout << "Parse failure.\n";

在这里,我们使用解析器 string_uint_uint,它产生一个 boost::parser::tuple<std::string, unsigned int, unsigned int> 属性。当我们尝试将其解析到输出参数 std::string 属性时,它就能正常工作。这是因为 std::string 有一个构造函数,它接受一个 std::string、一个偏移量和一个长度。这是另一个解析:

constexpr auto uint_string = bp::uint_ >> +bp::char_;
std::vector<std::string> vector_from_parse;
if (parse(input, uint_string, bp::ws, vector_from_parse)) {
    std::cout << "That yields this vector of strings:\n";
    for (auto && str : vector_from_parse) {
        std::cout << "  '" << str << "'\n";
    }
} else {
    std::cout << "Parse failure.\n";
}

现在我们有了解析器 uint_string,它产生 boost::parser::tuple<unsigned int, std::string> 属性——末尾的两个 char 组合成一个 std::string。这两个值可以通过计数,使用 T 构造函数来构建一个 std::vector<std::string>

就像在大多数地方使用聚合体代替元组一样,非聚合 class 类型也可以被替换为元组。这包括将非聚合 class 类型用作 rule 的属性类型。

但是,虽然兼容的元组可以被替换为聚合体,但你不能仅仅因为元组可以用来构造 T,就将一个元组替换为某个 class 类型 T。想想在上面第二个解析尝试中反转替换。将 std::vector<std::string> 转换为 boost::parser::tuple<unsigned int, std::string> 是没有意义的。

通常,你需要解析可能具有多种形式的内容。 operator| 被重载以形成备用解析器。例如

namespace bp = boost::parser;
auto const parser_1 = bp::int_ | bp::eps;

parser_1 匹配一个整数,或者如果失败,则匹配 epsilon,即空字符串。这等同于编写

namespace bp = boost::parser;
auto const parser_2 = -bp::int_;

然而,parser_1parser_2 都不等同于编写这个

namespace bp = boost::parser;
auto const parser_3 = bp::eps | bp::int_; // Does not do what you think.

原因是备用解析器会逐个尝试其子解析器,并停止在第一个匹配的解析器上。 Epsilon 匹配任何内容,因为它长度为零且不消耗任何输入。它甚至匹配输入的末尾。这意味着 parser_3 本身就等同于 eps

[Note] 注意

因此,对于任何解析器 p,编写 eps | p 被认为是一个 bug。在遇到 eps | p 时,调试版本将断言。

[Warning] 警告

这种错误在涉及 eps 时非常常见,而且也容易检测。但是,你可以编写 P1 >> P2,其中 P1P2 的前缀,例如 int_ | int >> int_,或者 repeat(4)[hex_digit] | repeat(8)[hex_digit]。这几乎肯定是一个错误,但在一般情况下无法检测——记住,rules 可以被单独编译,并考虑一对规则,它们的关联 _def 解析器分别是 int_int_ >> int_

解析带引号的字符串非常常见。然而,当使用跳过符时(你应该 99% 的时间都使用跳过符),带引号的字符串会有点棘手。你不想在字符串中间允许任意的空格,你也不想从字符串中移除所有空格。典型的跳过符 ws 会导致这两种情况都发生。

所以,大多数人会这样编写带引号的字符串解析器

namespace bp = boost::parser;
const auto string = bp::lexeme['"' >> *(bp::char_ - '"') > '"'];

需要注意的一些事项

  • 结果是一个字符串;
  • 引号不包含在结果中;
  • 在闭合引号前有一个期望点;
  • 使用 lexeme[] 会禁用解析器中的跳过,并且它必须写在引号周围,而不是写在 operator* 表达式周围;并且
  • 没有办法在字符串中间写入一个引号。

这是一个非常常见的模式。我已经写了数十次这样的带引号字符串解析器。上面的解析器是快速粗糙的版本。一个更健壮的版本将能够处理字符串中转义的引号,然后也会立即需要支持转义的转义字符。

Boost.Parser 提供了 quoted_string 来代替这个非常常见的模式。它支持引号和转义字符,使用反斜杠作为转义字符。

namespace bp = boost::parser;

auto result1 = bp::parse("\"some text\"", bp::quoted_string, bp::ws);
assert(result1);
std::cout << *result1 << "\n"; // Prints: some text

auto result2 = bp::parse(R"("some \"text\"")", bp::quoted_string, bp::ws);
assert(result2);
std::cout << *result2 << "\n"; // Prints: some "text"

尽管这个用例非常普遍,但仍有许多非常相似但它无法覆盖的用例。因此,quoted_string 有一些选项。如果你用一个字符调用它,它会返回一个 quoted_string,它将该字符用作引号字符。

auto result3 = bp::parse("!some text!", bp::quoted_string('!'), bp::ws);
assert(result3);
std::cout << *result3 << "\n"; // Prints: some text

你也可以提供一个字符范围。范围中的一个字符必须引用字符串的两端;不允许不匹配。想想 Python 如何允许你用 '"''\'' 来引用字符串,但两边必须使用相同的字符。

auto result4 = bp::parse("'some text'", bp::quoted_string("'\""), bp::ws);
assert(result4);
std::cout << *result4 << "\n"; // Prints: some text

在带引号的字符串解析器中做的另一件常见的事情是识别转义序列。如果你有不需要任何实际解析的简单转义序列,比如 C++ 的简单转义序列,你也可以提供一个 symbols 对象。 symbols<T> 的模板参数 T 必须是 charchar32_t。你不需要包含转义的反斜杠或转义的引号字符,因为它们总是有效的。

// the c++ simple escapes
bp::symbols<char> const escapes = {
    {"'", '\''},
    {"?", '\?'},
    {"a", '\a'},
    {"b", '\b'},
    {"f", '\f'},
    {"n", '\n'},
    {"r", '\r'},
    {"t", '\t'},
    {"v", '\v'}};
auto result5 =
    bp::parse("\"some text\r\"", bp::quoted_string('"', escapes), bp::ws);
assert(result5);
std::cout << *result5 << "\n"; // Prints (with a CRLF newline): some text

此外,对于上面显示的每种形式,你可以选择性地提供一个解析器作为最后一个参数,它将用于解析引号内的每个字符。你必须提供一个实际的完整解析器;你不能提供一个字符或字符串字面量。如果你不提供字符解析器,则使用 char_

auto result6 = bp::parse(
    "'some text'", bp::quoted_string("'\"", bp::char_('g')), bp::ws);
assert(!result6);
result6 =
    bp::parse("'gggg'", bp::quoted_string("'\"", bp::char_('g')), bp::ws);
assert(result6);
std::cout << *result6 << "\n"; // Prints: gggg

现在你已经看了一些例子,让我们更详细地看看解析是如何工作的。考虑这个例子。

namespace bp = boost::parser;
auto int_pair = bp::int_ >> bp::int_;         // Attribute: tuple<int, int>
auto int_pairs_plus = +int_pair >> bp::int_;  // Attribute: tuple<std::vector<tuple<int, int>>, int>

int_pairs_plus 必须匹配一个或多个 int 对(使用 int_pair),然后必须匹配一个额外的 int。换句话说,它匹配输入中的任何奇数(大于 1)个 int。我们来看看这个解析是如何进行的。

auto result = bp::parse("1 2 3", int_pairs_plus, bp::ws);

在解析开始时,顶层解析器使用其第一个子解析器(如果有)开始解析。因此,int_pairs_plus 是一个序列解析器,它会将控制权传递给它的第一个解析器 +int_pair。然后 +int_pair 将使用 int_pair 来进行解析,而 int_pair 又会使用 bp::int_。这创建了一个解析器堆栈,每个解析器使用一个特定的子解析器。

步骤 1) 输入是 "1 2 3",活动解析器堆栈是 int_pairs_plus -> +int_pair -> int_pair -> bp::int_。(将“->”读作“使用”。)这解析了 "1",并且之后的空格被 bp::ws 跳过。控制权传递给 int_pair 中的第二个 bp::int_ 解析器。

步骤 2) 输入是 "2 3",解析器堆栈看起来相同,只是活动解析器是 int_pair 中的第二个 bp::int_。该解析器消耗 "2",然后 bp::ws 跳过后面的空格。由于我们已经完成了 int_pair 的匹配,它的 boost::parser::tuple<int, int> 属性已完成。它的父级是 +int_pair,因此该元组属性被推送到 +int_pair 的属性后面,该属性是一个 std::vector<boost::parser::tuple<int, int>>。控制权传递到 int_pair 的父级 +int_pair。由于 +int_pair 是一个一次或多次解析器,它开始一个新的迭代;控制权再次传递给 int_pair

步骤 3) 输入再次是 "3",解析器堆栈看起来相同,只是活动解析器是 int_pair 中的第一个 bp::int_,并且我们处于 +int_pair 的第二次迭代中。该解析器消耗 "3"。由于这是输入的末尾,int_pair 的第二个 bp::int_ 没有匹配。这个部分的匹配 "3" 不应被计算,因为它不是完整匹配的一部分。因此,int_pair 指示其失败,并且 +int_pair 停止迭代。由于它确实匹配了一次,+int_pair 不会失败;它是一个零次或多次解析器;其子解析器在第一次成功后的失败不会导致其失败。控制权传递给 int_pairs_plus 中的下一个序列解析器。

步骤 4) 输入再次是 "3",解析器堆栈是 int_pairs_plus -> bp::int_。这解析了 "3",并且解析到达了输入末尾。控制权传递给 int_pairs_plus,它刚刚成功匹配了其序列中的所有解析器。然后它生成它的属性,一个 boost::parser::tuple<std::vector<boost::parser::tuple<int, int>>, int>,它被从 bp::parse() 返回。

关于步骤 #3 和 #4 之间需要注意的一点:在 #4 开始时,输入位置已经回退到 #3 开始时的位置。这种回溯发生在备用解析器中,当一个备用解析器失败时。在 P 解析过程中消耗的输入部分基本上被“归还”。

解析器详解

到目前为止,解析器被呈现为某种抽象实体。你可能想要更多细节。一个 Boost.Parser 解析器 P 是一个可调用的对象,它具有一对函数调用运算符重载。这两个函数非常相似,并且在许多解析器中,一个函数是基于另一个函数实现的。第一个函数进行解析并返回解析器的默认属性。第二个函数执行完全相同的解析,但接受一个输出参数,它将解析器的属性写入其中。输出参数不需要与默认属性相同,但它们需要兼容。

兼容性意味着默认属性在某种程度上可以赋值给输出参数。这通常意味着直接赋值,但也可能意味着元组 -> 聚合体或聚合体 -> 元组的转换。对于序列类型,兼容性意味着序列类型具有 insertpush_back 及其通常的语义。这意味着解析器 +boost::parser::int_ 可以像 std::vector<int> 一样填充 std::set<int>

一些解析器还有执行匹配所需的附加状态。例如,char_ 解析器可以被参数化为一个要匹配的单个代码点;该代码点的确切值存储在解析器对象中。

没有解析器直接支持解析器上定义的所有操作(operator|operator>> 等)。相反,有一个名为 parser_interface 的模板,它支持所有这些操作。 parser_interface 包装每个解析器,将其存储为数据成员,并将其改编为通用使用。你只应该在调试器中看到 parser_interface,或者可能在一些参考文档中。你不应该在自己的代码中编写它。

如前一页所述,当解析尝试匹配当前解析器 P,匹配了部分输入,但未能完全匹配 P 时,就会发生回溯。 P 解析过程中消耗的输入部分基本上被“归还”。

这是必需的,因为 P 可能由子解析器组成,每个成功的子解析器都会尝试消耗输入、生成属性等。当后面的子解析器失败时,P 的解析就会失败,并且输入必须被回退到 P 开始解析时的位置,而不是最新的匹配子解析器停止的位置。

备用解析器通常会逐个评估多个子解析器,向前移动然后恢复输入位置,直到其中一个子解析器成功。考虑这个例子。

namespace bp = boost::parser;
auto const parser = repeat(53)[other_parser] | repeat(10)[other_parser];

评估 parser 意味着尝试匹配 other_parser 53 次,如果失败,则尝试匹配 other_parser 10 次。假设你解析匹配 other_parser 11 次的输入。 parser 将匹配它。在解析过程中,它还将评估 other_parser 21 次。

repeat(53)[other_parser]repeat(10)[other_parser] 的属性分别是 std::vector<ATTR(other_parser);假设 ATTR(other_parser)int。整个 parser 的属性也是如此,std::vector<int>。由于 other_parser 正在忙于生成 int —— 确切地说是 21 个 —— 你可能会想,当 repeat(53)[other_parser] 无法找到所有 53 个输入时,在 repeat(53)[other_parser] 评估过程中生成的 int 会发生什么。届时,它的 std::vector<int> 将包含 11 个 int

当 repeat-parser 失败并生成属性时,它会清除其容器。这适用于上面的解析器,但也适用于所有其他 repeat 解析器,包括使用 operator+operator* 创建的解析器。

因此,在 parser 成功解析 10 个输入后(因为备用方案的右侧只消耗 10 次重复),parserstd::vector<int> 属性将包含 10 个 int

[Note] 注意

Boost.Spirit 的用户可能熟悉 hold[] 指令。由于上述行为,Boost.Parser 中没有这样的指令。

期望点

好了,那么如果解析器都尽力匹配输入,并且都是全有或全无的,这是否就为忽略各种错误输入留下了空间?考虑 解析 JSON 示例中的顶层解析器。

auto const value_p_def =
    number | bp::bool_ | null | string | array_p | object_p;

如果我用它来解析 "\"" 会怎样?解析尝试 number,失败。然后它尝试 bp::bool_,失败。然后 null 也失败。最后,它开始解析 string。好消息是,第一个字符是 JSON 字符串的开引号。不幸的是,这也是输入的末尾,所以 string 也必须失败。但是,我们可能不想在解析 string 现在就放弃,然后尝试 array_p,对吧?如果用户写了一个开引号但没有匹配的闭引号,那么它不是 value_p_def 的某个备用方案的前缀;它是格式错误的 JSON。这是 string 规则的解析器

auto const string_def = bp::lexeme['"' >> *(string_char - '"') > '"'];

注意,在右侧使用了 operator> 而不是 operator>>。这表示与 operator>> 相同的序列操作,不同的是它还表示一个期望。如果 operator> 前的解析成功,那么它后面的任何内容必须也成功。否则,顶层解析失败,并发出诊断信息。它会显示类似“这里期望 ‘"’。”的消息,引用该行,并用插入符号指向输入中期望右侧匹配的位置。

选择使用 > 而不是 >> 是你指示 Boost.Parser 解析失败是硬错误还是非硬错误的方式,分别是。

在编写解析器时,经常会遇到这样一种情况:有一组字符串,当解析它们时,它们与一组值一对一地关联。当需要将每个字符串与属性通过语义动作关联起来时,编写识别所有可能输入字符串的解析器会很繁琐。相反,我们可以使用符号表。

假设我们要解析罗马数字,这是最常见的与工作相关的解析问题之一。我们想识别以任意数量的“M”开头的数字,代表千位,后面跟着百位、十位和个位。其中任何一个都可能在输入中缺失,但并非全部。这里有三个符号 Boost.Parser 表,我们可以用它们分别识别个位、十位和百位值

bp::symbols<int> const ones = {
    {"I", 1},
    {"II", 2},
    {"III", 3},
    {"IV", 4},
    {"V", 5},
    {"VI", 6},
    {"VII", 7},
    {"VIII", 8},
    {"IX", 9}};

bp::symbols<int> const tens = {
    {"X", 10},
    {"XX", 20},
    {"XXX", 30},
    {"XL", 40},
    {"L", 50},
    {"LX", 60},
    {"LXX", 70},
    {"LXXX", 80},
    {"XC", 90}};

bp::symbols<int> const hundreds = {
    {"C", 100},
    {"CC", 200},
    {"CCC", 300},
    {"CD", 400},
    {"D", 500},
    {"DC", 600},
    {"DCC", 700},
    {"DCCC", 800},
    {"CM", 900}};

symbolschar 的字符串映射到其关联的属性。属性的类型必须指定为 symbols 的模板参数——在这种情况下是 int

遇到的任何“M”都应向结果添加 1000,所有其他值都来自符号表。这是我们需要做的语义动作

int result = 0;
auto const add_1000 = [&result](auto & ctx) { result += 1000; };
auto const add = [&result](auto & ctx) { result += _attr(ctx); };

add_1000 只是向 result 添加 1000add 将其解析器产生的任何属性添加到 result

现在我们只需要将这些片段组合起来创建一个解析器

using namespace bp::literals;
auto const parser =
    *'M'_l[add_1000] >> -hundreds[add] >> -tens[add] >> -ones[add];

这里我们引入了一些新东西,所以我们来分解一下。 'M'_l 是一个 字面量解析器。也就是说,它是一个解析字面量 char、代码点或字符串的解析器。在这种情况下,正在解析 char 'M'。末尾的 _l 部分是一个 UDL 后缀,你可以将其放在任何 charchar32_tchar const * 之后来形成字面量解析器。你也可以通过编写 lit(),传递上面提到的类型之一作为参数,来创建一个字面量解析器。

考虑到我们在之前的例子中使用了字面量 ',',为什么我们需要这个?原因在于 'M' 没有在与另一个 Boost.Parser 解析器一起的表达式中使用。它被用在 *'M'_l[add_1000] 中。如果我们写了 *'M'[add_1000],那显然是错误的;char 没有与之关联的 operator*,也没有 operator[]

[Tip] 提示

任何时候你想在 Boost.Parser 解析器中使用 charchar32_t 或字符串字面量,如果它与预先存在的 Boost.Parser 子解析器 p 组合,如 'x' >> p,则按原样编写。否则,你需要将其包装在对 lit() 的调用中,或使用 _l UDL 后缀。

接下来是 -hundreds[add]。到目前为止,索引运算符的使用应该非常熟悉了;它将语义动作 add 与解析器 hundreds 关联起来。前面的 operator- 是新的。它的意思是它所应用的解析器是可选的。你可以将其读作“零个或一个”。因此,如果 hundreds*'M'[add_1000] 之后没有被成功解析,什么都不会发生,因为 hundreds 是允许缺失的——它是可选的。如果 hundreds 被成功解析,比如匹配了 "CC",则在 add 内,产生的属性 200 会被添加到 result 中。

这是程序的完整列表。注意,在这里使用空格跳过符是不合适的,因为整个解析是一个单独的数字,所以它被删除了。

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main()
{
    std::cout << "Enter a number using Roman numerals. ";
    std::string input;
    std::getline(std::cin, input);

    bp::symbols<int> const ones = {
        {"I", 1},
        {"II", 2},
        {"III", 3},
        {"IV", 4},
        {"V", 5},
        {"VI", 6},
        {"VII", 7},
        {"VIII", 8},
        {"IX", 9}};

    bp::symbols<int> const tens = {
        {"X", 10},
        {"XX", 20},
        {"XXX", 30},
        {"XL", 40},
        {"L", 50},
        {"LX", 60},
        {"LXX", 70},
        {"LXXX", 80},
        {"XC", 90}};

    bp::symbols<int> const hundreds = {
        {"C", 100},
        {"CC", 200},
        {"CCC", 300},
        {"CD", 400},
        {"D", 500},
        {"DC", 600},
        {"DCC", 700},
        {"DCCC", 800},
        {"CM", 900}};

    int result = 0;
    auto const add_1000 = [&result](auto & ctx) { result += 1000; };
    auto const add = [&result](auto & ctx) { result += _attr(ctx); };

    using namespace bp::literals;
    auto const parser =
        *'M'_l[add_1000] >> -hundreds[add] >> -tens[add] >> -ones[add];

    if (bp::parse(input, parser) && result != 0)
        std::cout << "That's " << result << " in Arabic numerals.\n";
    else
        std::cout << "That's not a Roman number.\n";
}

[Important] 重要提示

symbols 在内部存储所有字符串为 UTF-32。如果你进行 Unicode 或 ASCII 解析,这对你来说完全无关紧要。如果你进行非 Unicode 解析,并且字符编码不是 Unicode 的子集(例如 EBCDIC),则可能会出现问题。有关更多信息,请参阅 Unicode 支持 部分。

诊断消息

就像 rule 一样,你可以给 symbols 提供一段诊断文本,当解析在期望点失败时,Boost.Parser 生成的错误消息将使用这段文本,如 错误处理和调试 中所述。有关详细信息,请参阅 symbols 构造函数。

前面的例子展示了如何使用符号表作为固定的查找表。如果我们想在解析过程中向表中添加内容怎么办?我们可以这样做,但必须在语义动作内部进行。首先,这是我们的符号表,已经包含了一个值

bp::symbols<int> const symbols = {{"c", 8}};
assert(parse("c", symbols));

不出所料,使用符号表作为解析器来解析符号表中的一个字符串是有效的。现在,这是我们的解析器

auto const parser = (bp::char_ >> bp::int_)[add_symbol] >> symbols;

在这里,我们将语义动作附加到了序列解析器 (bp::char_ >> bp::int_),而不是一个简单的解析器,如 double_。这个序列解析器包含两个解析器,每个都有自己的属性,因此它产生两个属性作为元组。

auto const add_symbol = [&symbols](auto & ctx) {
    using namespace bp::literals;
    // symbols::insert() requires a string, not a single character.
    char chars[2] = {_attr(ctx)[0_c], 0};
    symbols.insert(ctx, chars, _attr(ctx)[1_c]);
};

在语义动作内部,我们可以使用 Boost.Hana 提供的 UDLsboost::hana::tuple::operator[]() 来获取属性元组的第一个元素。第一个属性,来自 char_,是 _attr(ctx)[0_c],第二个属性,来自 int_,是 _attr(ctx)[1_c](如果 boost::parser::tuplestd::tuple 的别名,你将使用 std::getboost::parser::get 来代替)。要将符号添加到符号表,我们调用 insert()

auto const parser = (bp::char_ >> bp::int_)[add_symbol] >> symbols;

在解析过程中,("X", 9) 被解析并添加到符号表中。然后,第二个 'X' 被符号表解析器识别。但是

assert(!parse("X", symbols));

如果我们再次解析,我们会发现 "X" 没有保留在符号表中。 symbols 被声明为 const 这一事实可能已经暗示了这一点。

完整的程序:

#include <boost/parser/parser.hpp>

#include <iostream>
#include <string>


namespace bp = boost::parser;

int main()
{
    bp::symbols<int> const symbols = {{"c", 8}};
    assert(parse("c", symbols));

    auto const add_symbol = [&symbols](auto & ctx) {
        using namespace bp::literals;
        // symbols::insert() requires a string, not a single character.
        char chars[2] = {_attr(ctx)[0_c], 0};
        symbols.insert(ctx, chars, _attr(ctx)[1_c]);
    };
    auto const parser = (bp::char_ >> bp::int_)[add_symbol] >> symbols;

    auto const result = parse("X 9 X", parser, bp::ws);
    assert(result && *result == 9);
    (void)result;

    assert(!parse("X", symbols));
}

[Important] 重要提示

symbols 在内部存储所有字符串为 UTF-32。如果你进行 Unicode 或 ASCII 解析,这对你来说完全无关紧要。如果你进行非 Unicode 解析,并且字符编码不是 Unicode 的子集(例如 EBCDIC),则可能会出现问题。有关更多信息,请参阅 Unicode 支持 部分。

可以将符号永久添加到 symbols 中。要做到这一点,你必须使用一个可变的 symbols 对象 s,并通过调用 s.insert_for_next_parse() 来添加符号,而不是 s.insert()。这两个操作是正交的,所以如果你想将一个符号添加到当前顶级解析的表中,并将其保留在表中以供后续的顶级解析使用,你需要同时调用这两个函数。

还可以从符号表中擦除单个条目,或完全清除符号表。与插入一样,有适用于当前解析的擦除和清除版本,以及另一个仅适用于后续解析的版本。完整的操作集可以在 symbols API 文档中找到。

[Note] 注意

每个 symbols *_for_next_parse() 函数都有两个版本——一个带有上下文,一个不带。带上下文的版本用于语义动作中。不带上下文的版本用于在任何解析之外使用。

Boost.Parser 附带了所有解析器,能够满足大多数解析任务的需求。每个解析器都是一个 constexpr 对象,或者是一个 constexpr 函数。一些非函数对象也是可调用的,例如 char_,它可以直接使用,或带参数使用,例如 char_('a', 'z')。任何可调用的解析器,无论是函数还是可调用对象,从现在起都称为 可调用解析器。请注意,没有空参数的可调用解析器;它们都接受一个或多个参数。

每个可调用解析器都接受一个或多个 解析参数。解析参数可以是值,也可以是接受解析上下文引用的可调用对象。引用参数可以是可变的或常量的。例如

struct get_attribute
{
    template<typename Context>
    auto operator()(Context & ctx)
    {
        return _attr(ctx);
    }
};

这也可以是 lambda。例如

[](auto const & ctx) { return _attr(ctx); }

从解析参数(可以是值或接受解析上下文参数的可调用对象)生成值的操作,称为 解析 该解析参数。如果解析参数 arg 可以用当前上下文调用,那么 arg 的解析值是 arg(ctx);否则,解析值就是 arg

一些可调用解析器接受一个 解析谓词。解析谓词与解析参数不完全相同,因为它必须是可调用对象,而不能是值。解析谓词的返回类型必须可以上下文转换为 bool。例如

struct equals_three
{
    template<typename Context>
    bool operator()(Context const & ctx)
    {
        return _attr(ctx) == 3;
    }
};

这当然也可以是 lambda

[](auto & ctx) { return _attr(ctx) == 3; }

概念上的宏 RESOLVE() 扩展为解析参数或解析谓词的解析结果。你会在其余文档中看到它。

解析参数使用方式的示例

namespace bp = boost::parser;
// This parser matches one code point that is at least 'a', and at most
// the value of last_char, which comes from the globals.
auto last_char = [](auto & ctx) { return _globals(ctx).last_char; }
auto subparser = bp::char_('a', last_char);

暂时不用担心全局变量是什么;关键是你可以通过使用解析上下文,使传递给解析器的任何参数依赖于解析的当前状态。

namespace bp = boost::parser;
// This parser parses two code points.  For the parse to succeed, the
// second one must be >= 'a' and <= the first one.
auto set_last_char = [](auto & ctx) { _globals(ctx).last_char = _attr(x); };
auto parser = bp::char_[set_last_char] >> subparser;

每个可调用解析器都返回一个新的解析器,该解析器使用调用中给定的参数进行参数化。

此表列出了所有 Boost.Parser 解析器。对于可调用解析器,每个参数元数都有一个单独的条目。对于解析器 p,如果 p 没有参数的条目,则 p 是一个函数,它本身不能用作解析器;它必须被调用。在下表中

  • 每个条目都是一个全局对象,可直接在你的解析器中使用,除非下表另有说明;
  • “代码点”用于引用输入范围的元素,这假定解析是在 Unicode 感知代码路径中进行的(如果解析是在非 Unicode 代码路径中进行的,请将“代码点”读作“char”);
  • RESOLVE() 是一个概念上的宏,它扩展为解析参数的解析或解析谓词的评估(参见 解析器及其用法);
  • RESOLVE(pred) == true” 是“RESOLVE(pred) 可以上下文转换为 bool 并且 true”的简写;对于 false 也是如此;
  • c 是一个类型为 charchar8_tchar32_t 的字符;
  • str 是类型为 char const []char8_t const []char32_t const [] 的字符串字面量;
  • pred 是一个解析谓词;
  • arg0arg1arg2,... 是解析参数;
  • a 是一个语义动作;
  • r 是一个模拟 parsable_range 的对象;
  • pp1p2,... 是解析器;并且
  • escapes 是一个 symbols<T> 对象,其中 Tcharchar32_t
[Note] 注意

parsable_range 的定义是

template<typename T>
concept parsable_range = std::ranges::forward_range<T> &&
    code_unit<std::ranges::range_value_t<T>>;

[Note] 注意

此表中的一些解析器不消耗任何输入。除非下表另有说明,否则所有解析器都消耗它们匹配的输入。

表 25.6. 解析器及其语义

解析器

语义

属性类型

注意

eps

匹配 epsilon,即空字符串。总是匹配,并且不消耗输入。

无。

无限次匹配 eps 会产生无限循环,这在 C++ 中是未定义行为。当 Boost.Parser 遇到 *eps+eps 等时,在调试模式下会断言(这仅适用于无条件 eps)。

eps(pred)

如果 RESOLVE(pred) == false,则匹配失败。否则,其语义与 eps 相同。

无。

ws

根据 Unicode White_Space 属性,匹配单个空白代码点(参见注释)。

无。

更多信息,请参阅 Unicode 属性ws 可以消耗一个代码点或两个。它仅在匹配 "\r\n" 时消耗两个代码点。

eol

根据 Unicode 行断算法中的“硬”行中断,匹配单个换行符(参见注释)。

无。

更多信息,请参阅 Unicode 行断算法eol 可以消耗一个代码点或两个。它仅在匹配 "\r\n" 时消耗两个代码点。

eoi

仅在输入末尾匹配,并且不消耗输入。

无。

attr(arg0)

总是匹配,并且不消耗输入。生成属性 RESOLVE(arg0)

decltype(RESOLVE(arg0)).

attribute 的一个重要用例是提供一个默认属性值作为最后一个备选方案。例如,一个可选的逗号分隔列表是: int_ % ',' | attr(std::vector<int>)。没有“| attr(...)”,至少需要匹配一个 int_

char_

匹配任何单个代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 属性生成

char_(arg0)

精确匹配代码点 RESOLVE(arg0)

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 属性生成

char_(arg0, arg1)

如果 RESOLVE(arg0) <= n && n <= RESOLVE(arg1),则匹配输入中的下一个代码点 n

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 属性生成

char_(r)

如果 nr 中的一个代码点,则匹配输入中的下一个代码点 n

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 属性生成

r 被视为 UTF 编码。确切的 UTF 取决于 r 的元素类型。如果你不传递 UTF 编码的范围给 char_,则行为未定义。请注意,ASCII 是 UTF-8 的子集,所以 ASCII 可以。 EBCDIC 不行。 r 不会被复制;而是获取其引用。 char_(r) 的生命周期必须在 r 的生命周期内。此重载的 char_ 接受解析参数。

cp

匹配单个代码点。

char32_t

char_ 类似,但具有固定的 char32_t 属性类型;cp 具有与 char_ 相同的调用运算符重载,尽管为了简洁起见并未在此重复。

cu

匹配单个代码点。

char

char_ 类似,但具有固定的 char 属性类型;cu 具有与 char_ 相同的调用运算符重载,尽管为了简洁起见并未在此重复。即使名称“cu”暗示该解析器在代码单元级别匹配,但它并没有。该名称指的是生成的属性类型,类似于名称 int_uint_

blank

等同于 ws - eol

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

control

匹配单个控制字符代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

digit

匹配单个十进制数字代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

punct

匹配单个标点符号代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

symb

匹配单个符号代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

hex_digit

匹配单个十六进制数字代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

lower

匹配单个小写代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

upper

匹配单个大写代码点。

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char。请参阅 char_ 的条目。

lit(c)

精确匹配给定的代码点 c

无。

lit() 接受解析参数。

c_l

精确匹配给定的代码点 c

无。

这是一个 UDL,表示 lit(c),例如 'F'_l

lit(r)

精确匹配给定的字符串 r

无。

lit() 接受解析参数。

str_l

精确匹配给定的字符串 str

无。

这是一个 UDL,表示 lit(s),例如 "a string"_l

string(r)

精确匹配 r,并生成匹配作为属性。

std::string

string() 接受解析参数。

str_p

精确匹配 str,并生成匹配作为属性。

std::string

这是一个 UDL,表示 string(s),例如 "a string"_p

bool_

匹配 "true""false"

bool

bin

匹配二进制无符号整数值。

unsigned int

例如,bin 将匹配 "101",并生成属性 5u

bin(arg0)

精确匹配二进制无符号整数值 RESOLVE(arg0)

unsigned int

oct

匹配八进制无符号整数值。

unsigned int

例如,oct 将匹配 "31",并生成属性 25u

oct(arg0)

精确匹配八进制无符号整数值 RESOLVE(arg0)

unsigned int

hex

匹配十六进制无符号整数值。

unsigned int

例如,hex 将匹配 "ff",并生成属性 255u

hex(arg0)

精确匹配十六进制无符号整数值 RESOLVE(arg0)

unsigned int

ushort_

匹配无符号整数值。

unsigned short

ushort_(arg0)

精确匹配无符号整数值 RESOLVE(arg0)

unsigned short

uint_

匹配无符号整数值。

unsigned int

要指定基数/基数 N,请使用 uint_.base<N>()。要指定正好 D 位数字,请使用 uint_.digits<D>()。要指定最少 LO 位数字和最多 HI 位数字,请使用 uint_.digits<LO, HI>()。这些调用可以链式调用,例如 uint_.base<2>().digits<8>()

uint_(arg0)

精确匹配无符号整数值 RESOLVE(arg0)

unsigned int

ulong_

匹配无符号整数值。

unsigned long

ulong_(arg0)

精确匹配无符号整数值 RESOLVE(arg0)

unsigned long

ulong_long

匹配无符号整数值。

unsigned long long

ulong_long(arg0)

精确匹配无符号整数值 RESOLVE(arg0)

unsigned long long

short_

匹配有符号整数值。

short

short_(arg0)

精确匹配有符号整数值 RESOLVE(arg0)

short

int_

匹配有符号整数值。

int

要指定基数/基数 N,请使用 int_.base<N>()。要指定正好 D 位数字,请使用 int_.digits<D>()。要指定最少 LO 位数字和最多 HI 位数字,请使用 int_.digits<LO, HI>()。这些调用可以链式调用,例如 int_.base<2>().digits<8>()

int_(arg0)

精确匹配有符号整数值 RESOLVE(arg0)

int

long_

匹配有符号整数值。

long

long_(arg0)

精确匹配有符号整数值 RESOLVE(arg0)

long

long_long

匹配有符号整数值。

long long

long_long(arg0)

精确匹配有符号整数值 RESOLVE(arg0)

long long

float_

匹配浮点数。 float_ 使用 Boost.Spirit 的解析实现细节。关于接受哪些格式的详细信息,请参阅其 实数解析器。请注意,float_ 只支持默认的 RealPolicies

float

double_

匹配浮点数。 double_ 使用 Boost.Spirit 的解析实现细节。关于接受哪些格式的详细信息,请参阅其 实数解析器。请注意,double_ 只支持默认的 RealPolicies

double

repeat(arg0)[p]

p 精确匹配 RESOLVE(arg0) 次时匹配。

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

特殊值 Inf 可以被使用;它表示无限次重复。 decltype(RESOLVE(arg0)) 必须隐式转换为 int64_t。无限次匹配 eps 会产生无限循环,这在 C++ 中是未定义行为。当 Boost.Parser 遇到 repeat(Inf)[eps] 时,在调试模式下会断言(这仅适用于无条件 eps)。

repeat(arg0, arg1)[p]

p 匹配 RESOLVE(arg0)RESOLVE(arg1) 次(包含)之间时匹配。

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

特殊值 Inf 可用于上界;它表示无限次重复。 decltype(RESOLVE(arg0))decltype(RESOLVE(arg1)) 都必须隐式转换为 int64_t。无限次匹配 eps 会产生无限循环,这在 C++ 中是未定义行为。当 Boost.Parser 遇到 repeat(n, Inf)[eps] 时,在调试模式下会断言(这仅适用于无条件 eps)。

if_(pred)[p]

等同于 eps(pred) >> p

std::optional<ATTR(p)>

编写 if_(pred) 是一个错误。也就是说,省略条件匹配的解析器 p 是一个错误。

switch_(arg0)(arg1, p1)(arg2, p2) ...

相当于当 RESOLVE(arg0) == RESOLVE(arg1) 时的 p1,当 RESOLVE(arg0) == RESOLVE(arg2) 时的 p2,依此类推。如果不存在这样的 argN,则 switch_() 的行为是未定义的。

std::variant<ATTR(p1), ATTR(p2), ...>

switch_()(arg0) 是一个错误。也就是说,省略条件匹配的解析器 p1p2、... 是一个错误。

symbols<T>

symbols 是一个键值对的关联容器。每个键都是 std::string,每个值都有类型 T。在 Unicode 解析路径中,字符串被认为是 UTF-8 编码的;在非 Unicode 路径中,不假定任何编码。symbols 匹配输入的 pre 的最长前缀,该前缀等于键 k 之一。如果 pre 的长度 len 为零,并且没有零长度的键,则不匹配输入。如果 len 为正,则生成的属性是与 k 关联的值。

T

与表中其他条目不同,symbols 是一个类型,而不是一个对象。

quoted_string

匹配 '"',后跟零个或多个字符,后跟 '"'

std::string

结果不包括引号。字符串中的引号可以通过反斜杠转义来写。字符串中的反斜杠可以通过写两个连续的反斜杠来写。任何其他使用反斜杠的方式都会导致解析失败。在解析整个字符串时,跳过功能被禁用,就好像使用了 lexeme[] 一样。

quoted_string(c)

匹配 c,后跟零个或多个字符,后跟 c

std::string

结果不包括 c 引号。字符串中的 c 可以通过反斜杠转义来写。字符串中的反斜杠可以通过写两个连续的反斜杠来写。任何其他使用反斜杠的方式都会导致解析失败。在解析整个字符串时,跳过功能被禁用,就好像使用了 lexeme[] 一样。

quoted_string(r)

匹配 r 中的某个字符 Q,后跟零个或多个字符,后跟 Q

std::string

结果不包括 Q 引号。字符串中的 Q 可以通过反斜杠转义来写。字符串中的反斜杠可以通过写两个连续的反斜杠来写。任何其他使用反斜杠的方式都会导致解析失败。在解析整个字符串时,跳过功能被禁用,就好像使用了 lexeme[] 一样。

quoted_string(c, symbols)

匹配 c,后跟零个或多个字符,后跟 c

std::string

结果不包括 c 引号。字符串中的 c 可以通过反斜杠转义来写。字符串中的反斜杠可以通过写两个连续的反斜杠来写。反斜杠后跟使用 symbols 的成功匹配将被解释为 symbols 产生的相应值。任何其他使用反斜杠的方式都会导致解析失败。在解析整个字符串时,跳过功能被禁用,就好像使用了 lexeme[] 一样。

quoted_string(r, symbols)

匹配 r 中的某个字符 Q,后跟零个或多个字符,后跟 Q

std::string

结果不包括 Q 引号。字符串中的 Q 可以通过反斜杠转义来写。字符串中的反斜杠可以通过写两个连续的反斜杠来写。反斜杠后跟使用 symbols 的成功匹配将被解释为 symbols 产生的相应值。任何其他使用反斜杠的方式都会导致解析失败。在解析整个字符串时,跳过功能被禁用,就好像使用了 lexeme[] 一样。


[Important] 重要提示

所有字符解析器,如 char_cpcu,都产生 charchar32_t 属性。因此,当你看到上面表格中的“如果 ATTR(p)charchar32_t,则为 std::string,否则为 std::vector<ATTR(p)>”时,这实际上意味着每个字符属性序列都被转换成了一个 std::string。唯一不发生这种情况的是当你使用你自己的规则并带有其他字符类型的属性时(或使用 attribute 来实现)。

[Note] 注意

这些解析器的属性的更完整的描述在后续章节中。属性在此处重复,以便你可以在一个地方查看解析器的所有属性。

如果你有一个 Boost.Parser 解析器未覆盖的整数类型 IntType,你可以显式指定基数/进制或数字的位数。你可以通过在正确整数类型的现有解析器上调用 base()digits() 成员函数来实现。所以如果 IntType 是无符号的,你会使用 uint_。如果是带符号的,你会使用 int_。例如

constexpr auto hex_int = bp::uint_.base<16>();

你只需将你想要的约束链接在一起,例如 .base<16>().digits<2>().digits<4>().base<8>()

所以,如果你想解析八个十六进制数字连续出现,以识别 C++ 那样的 Unicode 字符字面量(例如 \Udeadbeef),你可以使用这个解析器来处理末尾的数字。

constexpr auto hex_4_def = bp::uint_.base<16>().digits<8>();

如果你想指定一个可接受的数字范围,请使用 .digits<LO, HI>()HILO 都是包含边界。

指令是你解析器的一个元素,它本身没有意义。有些是二阶解析器,需要一阶解析器来完成实际的解析。其他则以某种方式影响解析。你通常可以通过使用 [] 来在词法上识别指令;指令总是使用 []。非指令也可能使用,但仅在附加语义动作时使用。

二阶解析器的指令在技术上是指令,但由于它们也用于创建解析器,因此更关注这一点更有用。本节已在解析器部分描述了 repeat()if_() 指令;我们在这里不会过多介绍它们。

与序列、选择和排列解析器的交互

在大多数情况下,序列、选择和排列解析器不会嵌套。(为了简单起见,我们只考虑序列解析器,但大部分逻辑也适用于选择解析器。) a >> b >> c(a >> b) >> ca >> (b >> c) 相同,它们都由一个具有三个子解析器 abcseq_parser 表示。然而,如果某些东西阻止两个 seq_parsers 直接交互,它们嵌套。例如,lexeme[a >> b] >> c 是一个 seq_parser,包含两个解析器 lexeme[a >> b]c。这是因为 lexeme[] 获取其给定的解析器并将其包装在一个 lexeme_parser 中。这反过来又关闭了序列解析器的组合逻辑,因为 lexeme[a >> b] >> c 中的第二个 operator>> 的两侧都不是 seq_parsers。序列解析器有几个规则,根据其子解析器的位置和属性来确定解析器的整体属性类型(参见 Attribute Generation)。因此,了解哪些指令创建新解析器(以及哪种类型),哪些不创建新解析器很重要;这在下面为每个指令指示。

指令
repeat()

参见 The Parsers And Their Uses。创建一个 repeat_parser

if_()

参见 The Parsers And Their Uses。创建一个 seq_parser

omit[]

omit[p] 禁用解析器 p 的属性生成。不仅 omit[][p] 没有属性,而且 p 中通常发生的任何属性生成工作都会被跳过。

这个指令在以下情况下可能有用:假设你有一个相当复杂的解析器 p,它生成一个大型且构造昂贵的属性。现在假设你想写一个函数来计算 p 在字符串中可以匹配多少次(匹配是非重叠的)。与其直接使用 p 并构建所有这些属性,或者重写 p 而不生成属性,不如使用 omit[]

创建一个 omit_parser

raw[]

raw[p] 将属性从 ATTR(p) 更改为表示 p 匹配的输入子范围的视图。视图的类型是 subrange<I>,其中 I 是解析器内使用的迭代器类型。请注意,这可能与传递给 parse() 的迭代器类型不同。例如,在解析 UTF-8 时,传递给 parse() 的迭代器可能是 char8_t const *,但在解析过程中,它将是一个 UTF-8 到 UTF-32 的转码(转换)迭代器。与 omit[] 类似,raw[] 会跳过 p 内的所有属性生成工作。

类似于上面 omit[] 的重用场景,raw[] 可用于查找字符串中 p 的所有非重叠匹配的位置

创建一个 raw_parser

string_view[]

string_view[p]raw[p] 非常相似,不同之处在于它将 p 的属性更改为 std::basic_string_view<C>,其中 C 是被解析的底层范围的字符类型。string_view[] 要求被解析的底层范围是连续的。由于这仅在 C++20 及更高版本中才能检测到,因此 string_view[] 在 C++17 模式下不可用。

类似于上面 omit[] 的重用场景,string_view[] 可用于查找字符串中 p 的所有非重叠匹配的位置。无论 raw[] 还是 string_view[] 更适合用于报告位置,它们本质上是相同的。

创建一个 string_view_parser

no_case[]

no_case[p] 在解析 p 的过程中启用不区分大小写的解析。这适用于 char_()string()bool_ 解析器解析的文本。数字解析器已经不区分大小写了。不区分大小写是通过对要解析的文本和要匹配的解析器中的值进行 Unicode 大小写折叠来实现的(如果你想了解更多关于 Unicode 大小写折叠的信息,请参见下面的注释)。在非 Unicode 代码路径中,不执行完整的 Unicode 大小写折叠;相反,只执行小于 0x100 的值的转换。示例

#include <boost/parser/transcode_view.hpp> // For as_utfN.

namespace bp = boost::parser;
auto const street_parser = bp::string(u8"Tobias Straße");
assert(!bp::parse("Tobias Strasse" | bp::as_utf32, street_parser));             // No match.
assert(bp::parse("Tobias Strasse" | bp::as_utf32, bp::no_case[street_parser])); // Match!

auto const alpha_parser = bp::no_case[bp::char_('a', 'z')];
assert(bp::parse("a" | bp::as_utf32, bp::no_case[alpha_parser])); // Match!
assert(bp::parse("B" | bp::as_utf32, bp::no_case[alpha_parser])); // Match!

no_case[] 中的一切几乎都如你 naively 期望的那样工作,除了 char_ 的两字符范围版本有一个限制。它只比较输入中的一个码点和它的两个参数(例如上面示例中的 'a''z')。它不对多码点大小写折叠扩展做任何特殊处理。例如,char_(U'ß', U'ß') 匹配输入 U"s",这是有意义的,因为 U'ß' 扩展为 U"ss"。但是,相同的解析器匹配输入 U"ß"!总之,坚持使用具有单码点大小写折叠扩展的码点对。如果你需要支持多扩展码点,请使用另一个重载,例如:char_(U"abcd/*...*/ß")

[Note] 注意

Unicode 大小写折叠是一个将文本统一为一种大小写的操作,如果你对两个文本 AB 进行此操作,那么你可以按位比较它们以查看它们是否相同,除了大小写。大小写折叠有时会将一个码点扩展为多个码点(例如,大小写折叠 "ẞ" 得到 "ss"。当发生这种多码点扩展时,扩展后的码点处于 NFKC 规范化形式。

创建一个 no_case_parser

lexeme[]

lexeme[p] 在解析 p 时禁用跳过器的使用,如果正在使用跳过器的话。例如,如果你想在你的解析器的绝大多数部分启用跳过,但只在一个不适合的地方禁用它。如果你在你的解析器的大部分地方跳过空格,但想解析可能包含空格的字符串,你应该使用 lexeme[]

namespace bp = boost::parser;
auto const string_parser = bp::lexeme['"' >> *(bp::char_ - '"') >> '"'];

如果没有 lexeme[],我们的字符串解析器可以正确匹配 "foo bar",但生成的属性将是 "foobar"

创建一个 lexeme_parser

skip[]

skip[] 类似于 lexeme[] 的反向。它在解析中启用跳过,即使它之前没有启用。例如,在调用使用跳过器的 parse() 时,假设我们正在使用这些解析器

namespace bp = boost::parser;
auto const one_or_more = +bp::char_;
auto const skip_or_skip_not_there_is_no_try = bp::lexeme[bp::skip[one_or_more] >> one_or_more];

lexeme[] 的使用禁用了跳过,但 skip[] 的使用又将其重新启用。最终结果是 one_or_more 的第一次出现将使用传递给 parse() 的跳过器;第二次出现则不会。

skip[] 还有其他用途。你可以用另一个解析器参数化 skip 来更改跳过器,仅在指令的作用域内。假设我们向 parse() 传递了 ws,并且我们在该 parse() 调用中的某个地方使用了这些解析器。

namespace bp = boost::parser;
auto const zero_or_more = *bp::char_;
auto const skip_both_ways = zero_or_more >> bp::skip(bp::blank)[zero_or_more];

第一次出现 zero_or_more 将使用传递给 parse() 的跳过器,即 ws;第二次出现将使用 blank 作为其跳过器。

创建一个 skip_parser

merge[], separate[], and transform(f)[]

这些指令影响属性的生成。有关它们的更多详细信息,请参见 Attribute Generation 部分。

merge[]separate[] 创建给定 seq_parser 的副本。

transform(f)[] 创建一个 tranform_parser

delimiter(p)[]

delimiter 指令允许在排列解析器中使用分隔符。它适用于排列解析器,就像 merge[]separate[] 仅适用于序列解析器一样。考虑这个排列解析器。

constexpr auto parser = bp::int_ || bp::string("foo") || bp::char_('g');

这将匹配所有:一个整数、"foo"'g',以任何顺序(例如,"foo g 42")。如果你还想用逗号分隔这三个元素,你可以这样写解析器。

constexpr auto delimited_parser =
    bp::delimiter(bp::lit(','))[bp::int_ || bp::string("foo") || bp::char_('g')];

delimited_parser 将解析与 parser 相同的元素,但还会要求元素之间有逗号(例如 "foo, g, 42")。

Boost.Parser 中的所有解析器都定义了某些重载运算符。在本教程中,我们已经看到了一些,特别是 operator>>operator|operator||,它们分别用于形成序列解析器、选择解析器和排列解析器。

以下是为解析器重载的所有运算符。在下面的表格中

  • c 是类型为 charchar32_t 的字符;
  • a 是一个语义动作;
  • r 是一个对象,其类型模拟 parsable_range(参见 Concepts);并且
  • pp1p2、... 是解析器。
[Note] 注意

此表中的某些表达式不消耗输入。所有解析器都会消耗它们匹配的输入,除非在下面的表中另有说明。

表 25.7. 组合操作及其语义

表达式

语义

属性类型

注意

!p

当且仅当 p 不匹配时匹配;不消耗输入。

无。

&p

当且仅当 p 匹配时匹配;不消耗输入。

无。

*p

反复使用 p 进行解析,直到 p 不再匹配;始终匹配。

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

无限制次数地匹配 eps 会产生无限循环,这在 C++ 中是未定义行为。当遇到 *eps 时,Boost.Parser 会在调试模式下断言(这仅适用于无条件 eps)。

+p

反复使用 p 进行解析,直到 p 不再匹配;当且仅当 p 至少匹配一次时匹配。

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

无限制次数地匹配 eps 会产生无限循环,这在 C++ 中是未定义行为。当遇到 +eps 时,Boost.Parser 会在调试模式下断言(这仅适用于无条件 eps)。

-p

相当于 p | eps

std::optional<ATTR(p)>

p1 >> p2

当且仅当 p1 匹配然后 p2 匹配时匹配。

boost::parser::tuple<ATTR(p1), ATTR(p2)> (参见注释)。

>> 是关联的;p1 >> p2 >> p3(p1 >> p2) >> p3p1 >> (p2 >> p3) 都等价。此属性类型仅适用于 p1p2 都生成属性的情况;请参见 Attribute Generation 以获取完整规则。其优先级与 operator> 不同。

p >> c

相当于 p >> lit(c)

ATTR(p)

p >> r

相当于 p >> lit(r)

ATTR(p)

p1 > p2

当且仅当 p1 匹配然后 p2 匹配时匹配。在 p1 匹配后不允许回溯;如果 p1 匹配但 p2 不匹配,则顶层解析失败。

boost::parser::tuple<ATTR(p1), ATTR(p2)> (参见注释)。

> 是关联的;p1 > p2 > p3(p1 > p2) > p3p1 > (p2 > p3) 都等价。此属性类型仅适用于 p1p2 都生成属性的情况;请参见 Attribute Generation 以获取完整规则。其优先级与 operator>> 不同。

p > c

相当于 p > lit(c)

ATTR(p)

p > r

相当于 p > lit(r)

ATTR(p)

p1 | p2

当且仅当 p1 匹配或 p2 匹配时匹配。

std::variant<ATTR(p1), ATTR(p2)> (参见注释)。

| 是关联的;p1 | p2 | p3(p1 | p2) | p3p1 | (p2 | p3) 都等价。此属性类型仅适用于 p1p2 都生成属性且属性类型不同的情况;请参见 Attribute Generation 以获取完整规则。

p | c

相当于 p | lit(c)

ATTR(p)

p | r

相当于 p | lit(r)

ATTR(p)

p1 || p2

当且仅当 p1 匹配且 p2 匹配时匹配,无论它们匹配的顺序如何。

boost::parser::tuple<ATTR(p1), ATTR(p2)>

|| 是关联的;p1 || p2 || p3(p1 || p2) || p3p1 || (p2 || p3) 都等价。在 operator|| 表达式中包含 eps(条件式或非条件式)是错误的。虽然解析器按任何顺序匹配,但属性元素始终按 operator|| 表达式中书写的顺序排列。

p1 - p2

相当于 !p2 >> p1

ATTR(p1)

p - c

相当于 p - lit(c)

ATTR(p)

p - r

相当于 p - lit(r)

ATTR(p)

p1 % p2

相当于 p1 >> *(p2 >> p1)

std::string 如果 ATTR(p)charchar32_t,否则为 std::vector<ATTR(p1)>

p % c

相当于 p % lit(c)

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

p % r

相当于 p % lit(r)

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

p[a]

当且仅当 p 匹配时匹配。如果 p 匹配,则执行语义动作 a

无。


[Important] 重要提示

所有字符解析器,如 char_cpcu,都产生 charchar32_t 属性。因此,当你看到上面表格中的“如果 ATTR(p)charchar32_t,则为 std::string,否则为 std::vector<ATTR(p)>”时,这实际上意味着每个字符属性序列都被转换成了一个 std::string。唯一不发生这种情况的是当你使用你自己的规则并带有其他字符类型的属性时(或使用 attribute 来实现)。

上面表格中没有捕获到几条特殊规则

首先,零次或多次重复和一次或多次重复(operator*()operator+(),分别)在组合时可能会折叠。对于任何解析器 p+(+p) 折叠为 +p**p*+p+*p 都折叠为 *p

其次,在选择解析器中使用 eps 作为最后一个选项以外的任何选项都是常见的错误来源;Boost.Parser 禁止它。这是因为,对于任何解析器 peps | p 等同于 eps,因为 eps 总是匹配。对于带有条件的 eps,情况并非如此。对于任何条件 condeps(cond) 允许出现在选择解析器中的任何位置。

[Important] 重要提示

C++ 运算符 >>> 的优先级不同。这有时会在编译器警告中出现。无论你如何对由 >>> 分隔的解析器链进行分组或不分组,最终的表达式都会得到相同的计算结果。如果你的编译器抱怨,请随时添加括号。更广泛地说,在编写解析器时,请牢记 C++ 运算符优先级规则——最简单的写法可能不具有你期望的语义。

[Note] 注意

当在调试器中查看 Boost.Parser 解析器时,或者在查看它们的参考文档时,你可能会看到对模板 parser_interface 的引用。这个模板是为了提供上面描述的运算符重载。它允许解析器本身非常简单——大多数解析器只是一个带有两个成员函数的结构。parser_interface 在使用 Boost.Parser 时基本上是不可见的,而且你在自己的代码中永远不需要命名这个模板。

到目前为止,我们已经看到了来自不同解析器的几种不同类型的属性,例如 int_intboost::parser::char_ >> boost::parser::int_boost::parser::tuple<char, int>,等等。让我们更严谨地深入了解它是如何工作的。

[Note] 注意

有些解析器根本没有属性。在下面的表格中,属性类型被列为“None”。有一个非 void 类型从每个没有属性的解析器返回。这使得逻辑简单;必须处理两种情况——void 或非 void——会使库复杂得多。这些解析器关联的非 void 属性的类型是实现细节。类型来自 boost::parser::detail 命名空间,并且非常无用。你永远不应该在实践中看到这个类型。在语义动作中,请求一个不产生属性的解析器的属性(使用 _attr(ctx))将产生一个特殊类型 boost::parser::none 的值。当以返回已解析属性的形式调用 parse() 时,如果没有属性,则只返回 bool;这表示解析的成功或失败。

[Warning] 警告

Boost.Parser 假设所有属性都是半正则的(参见 std::semiregular)。在 Boost.Parser 代码中,属性被赋值、移动、复制和默认构造。不支持仅移动类型或不可默认构造的类型。

属性类型特征,attribute

你可以使用 attribute(以及相关的别名 attribute_t)来确定如果一个解析器被传递给 parse() 会产生什么属性。由于至少有一个解析器(char_)具有多态属性类型,因此 attribute 也需要被解析的范围类型。如果一个解析器不产生属性,attribute 将产生 none,而不是 void

如果你想将迭代器/哨兵对馈送到 attribute,请像这样创建一个范围:

constexpr auto parser = /* ... */;
auto first = /* ... */;
auto const last = /* ... */;

namespace bp = boost::parser;
// You can of course use std::ranges::subrange directly in C++20 and later.
using attr_type = bp::attribute_t<decltype(BOOST_PARSER_SUBRANGE(first, last)), decltype(parser)>;

由于没有任何一个解析器具有单一的属性类型,因为一个解析器可以放在 omit[] 中,这使得它的属性类型为 none。因此,attribute 无法告诉你解析器在所有情况下会产生什么属性;它只能告诉你如果它被传递给 parse() 会产生什么属性。

解析器属性

此表总结了所有 Boost.Parser 解析器生成的属性。在下面的表格中

  • RESOLVE() 是一个名义上的宏,它展开为解析参数的解析或解析谓词的求值(参见 The Parsers And Their Uses);并且
  • xy 代表任意对象。

表 25.8. 解析器及其属性

解析器

属性类型

注意

eps

无。

eol

无。

eoi

无。

attr(x)

decltype(RESOLVE(x))

char_

Unicode 解析中的代码点类型,或非 Unicode 解析中的 char;参见下文。

包括所有接受单个字符的 _p UDLs,以及所有字符类解析器,如 controllower

cp

char32_t

cu

char

lit(x)

无。

包括所有接受字符串的 _l UDLs

string(x)

std::string

包括所有接受字符串的 _p UDLs

bool_

bool

bin

unsigned int

oct

unsigned int

hex

unsigned int

ushort_

unsigned short

uint_

unsigned int

ulong_

unsigned long

ulong_long

unsigned long long

short_

short

int_

int

long_

long

long_long

long long

float_

float

double_

double

symbols<T>

T


char_ 有点奇怪,因为它有一个多态属性类型。当你在非 Unicode 代码路径(即 char 字符串)中使用 char_ 解析文本时,属性是 char。当你使用完全相同的 char_ 在 Unicode 感知的代码路径中解析时,所有匹配都是基于代码点的,所以属性类型是用于表示代码点的类型 char32_t。所有 UTF-8 的解析都属于这种情况。

这里,我们解析的是纯 char,这意味着解析在非 Unicode 代码路径中,char_ 的属性是 char

auto result = parse("some text", boost::parser::char_);
static_assert(std::is_same_v<decltype(result), std::optional<char>>));

当你解析 UTF-8 时,匹配是基于代码点进行的,因此属性类型是 char32_t

auto result = parse("some text" | boost::parser::as_utf8, boost::parser::char_);
static_assert(std::is_same_v<decltype(result), std::optional<char32_t>>));

好消息是,通常你不会单独解析字符。当你使用 char_ 解析时,你通常会解析它们的重复,无论你是在 Unicode 解析模式下还是不在,这都会生成一个 std::string。如果你确实需要解析单个字符,并想锁定它们的属性类型,你可以使用 cp 和/或 cu 来强制使用非多态属性类型。

组合操作属性

组合操作当然会影响属性的生成。在下面的表格中

  • mn 是解析参数,解析为整数值;
  • pred 是一个解析谓词;
  • arg0arg1arg2,... 是解析参数;
  • a 是一个语义动作;并且
  • pp1p2、... 是生成属性的解析器。

表 25.9. 组合操作及其属性

解析器

属性类型

!p

无。

&p

无。

*p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

+p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

+*p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

*+p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

-p

std::optional<ATTR(p)>

p1 >> p2

boost::parser::tuple<ATTR(p1), ATTR(p2)>

p1 > p2

boost::parser::tuple<ATTR(p1), ATTR(p2)>

p1 >> p2 >> p3

boost::parser::tuple<ATTR(p1), ATTR(p2), ATTR(p3)>

p1 > p2 >> p3

boost::parser::tuple<ATTR(p1), ATTR(p2), ATTR(p3)>

p1 >> p2 > p3

boost::parser::tuple<ATTR(p1), ATTR(p2), ATTR(p3)>

p1 > p2 > p3

boost::parser::tuple<ATTR(p1), ATTR(p2), ATTR(p3)>

p1 | p2

std::variant<ATTR(p1), ATTR(p2)>

p1 | p2 | p3

std::variant<ATTR(p1), ATTR(p2), ATTR(p3)>

p1 || p2

boost::parser::tuple<ATTR(p1), ATTR(p2)>

p1 || p2 || p3

boost::parser::tuple<ATTR(p1), ATTR(p2), ATTR(p3)>

p1 % p2

std::string 如果 ATTR(p1)charchar32_t,否则为 std::vector<ATTR(p1)>

p[a]

无。

repeat(arg0)[p]

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

repeat(arg0, arg1)[p]

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

if_(pred)[p]

std::optional<ATTR(p)>

switch_(arg0)(arg1, p1)(arg2, p2)...

std::variant<ATTR(p1), ATTR(p2), ...>


[Important] 重要提示

所有字符解析器,如 char_cpcu,都产生 charchar32_t 属性。因此,当你看到上面表格中的“如果 ATTR(p)charchar32_t,则为 std::string,否则为 std::vector<ATTR(p)>”时,这实际上意味着每个字符属性序列都被转换成了一个 std::string。唯一不发生这种情况的是当你使用你自己的规则并带有其他字符类型的属性时(或使用 attribute 来实现)。

[Important] 重要提示

如果您没有注意到,在解析器中添加语义动作会擦除解析器的属性。属性在语义动作中仍可用,形式为 _attr(ctx)

有相对较少的规则定义了序列解析器和替代解析器属性是如何生成的。(别担心,下面有例子。)

序列解析器属性规则

序列解析器的属性生成行为概念上很简单。

  • 子解析器的属性形成一个值的元组;
  • 不生成属性的子解析器不计入序列的属性;
  • 通常生成属性的子解析器为元组结果贡献一个单独的元素;除非
  • 当相同元素类型的容器相邻出现,或者单个元素与其类型的容器相邻出现时,这两个相邻的属性会合并成一个属性;并且
  • 如果所有这些的结果是一个退化的元组 boost::parser::tuple<T>(即使 T 是表示“无属性”的类型),属性会变为 T

更正式地说,属性生成算法如下。对于序列解析器 p,令子解析器的属性类型列表为 a0, a1, a2, ..., an

我们通过计算一个编译时左折叠操作来得到 p 的属性,即 left-fold({a1, a2, ..., an}, tuple<a0>, OP)OP 是组合操作,它接收当前属性类型(初始为 boost::parser::tuple<a0>)和下一个属性类型,并返回新的当前属性类型。折叠操作结束时的当前属性类型就是 p 的属性类型。

OP 尝试逐个应用一系列规则。规则表示为 X >> Y -> Z,其中 X 是当前属性的类型,Y 是下一个属性的类型,Z 是新的当前属性类型。在这些规则中,C<T>T 的容器;none 是表示无属性的特殊类型;T 是一个类型;CHAR 是一个字符类型,即 charchar32_t;而 Ts... 是一个包含一个或多个类型的参数包。请注意,T 可能是特殊类型 none。当前属性始终是一个元组(称为 Tup),因此“当前属性 X”指的是 Tup 的最后一个元素,而不是 Tup 本身,除非是那些明确提及 boost::parser::tuple<> 作为 X 类型一部分的规则。

合并容器和(可能可选的)相邻值的规则(例如 C<T> >> optional<T> -> C<T>)对字符串有一个特殊情况。如果 C<T> 正好是 std::string,并且 Tcharchar32_t,则组合结果是一个 std::string

同样,如果最终结果是属性为 boost::parser::tuple<T>,属性将变为 T

[Note] 注意

在上述规则中,什么构成容器由 container 概念确定。

template<typename T>
concept container = std::ranges::common_range<T> && requires(T t) {
    { t.insert(t.begin(), *t.begin()) }
        -> std::same_as<std::ranges::iterator_t<T>>;
};

序列解析器属性示例

请注意,OP 的应用是采用左折叠的风格,因此是贪婪的。这可能导致一些不直观的结果。例如,考虑以下程序。感谢 Duncan Paterson 提供的这个非常好的例子!

#include <boost/parser/parser.hpp>
#include <print>

namespace bp = boost::parser;
int main() {
  const auto id_set_action = [](auto &ctx) {
    const auto& [left, right] = _attr(ctx);
    std::println("{} = {}", left, right);
  };

  const auto id_parser = bp::char_('a', 'z') > *bp::char_('a', 'z');

  const auto id_set = (id_parser >> '=' >> id_parser)[id_set_action];
  bp::parse("left=right", id_set);
  return 0;
}

也许令人惊讶的是,这个程序打印 leftr = ight!为什么会这样?这是因为 id_parser 似乎施加了结构,但实际上并没有。 id_set 完全等同于(已添加注释以澄清以下部分是哪些):

const auto id_set = (
  /*A*/ bp::char_('a', 'z') > /*B*/ *bp::char_('a', 'z') >>
  /*C*/ '=' >>
  /*D*/ bp::char_('a', 'z') > /*E*/ *bp::char_('a', 'z')
)[id_set_action];

当 Boost.Parser 将 OP 应用于此序列解析器时,各个步骤是:AB 合并成单个 std::stringC 被忽略,因为它不生成属性;D 被合并到由 AB 形成的 std::string 中;最后,我们得到 EE 不会与 D 合并,因为 D 已经被消耗了。E 也不会与我们从 ABD 形成的 std::string 合并,因为我们不合并相邻的容器。最后,我们得到一个包含两个 std::string 的元组,其中第一个元素包含 ABD 解析的所有字符,第二个元素包含 E 解析的所有字符。

这显然不是我们想要的。如何得到一个打印 left = right 的顶级解析器?我们使用 rule。在 rule 中使用的解析器永远不能与 rule 外部的任何解析器合并。规则的实例与其使用它们的解析器是本质上独立的,无论这些解析器是 rules 还是非 rule 解析器。因此,考虑一个等价于前面 id_parserrule

namespace bp = boost::parser;
bp::rule<struct id_parser_tag, std::string> id_parser = "identifier";
auto const id_parser_def = bp::char_('a', 'z') > *bp::char_('a', 'z');
BOOST_PARSER_DEFINE_RULES(id_parser);

之后,我们可以像使用前面非规则版本一样使用它。

const auto id_set = (id_parser >> '=' >> id_parser)[id_set_action];

这产生了您可能期望的结果,因为只有 rule id_parser 内的 bp::char_('a', 'z') > *bp::char_('a', 'z') 解析器才有资格通过 OP 进行组合。

替代解析器属性规则

替代解析器的规则要简单得多。对于替代解析器 p,令子解析器的属性类型列表为 a0, a1, a2, ..., anp 的属性是 std::variant<a0, a1, a2, ..., an>,并应用以下步骤

  • 所有 none 属性都被排除,如果存在,则属性被包装在一个 std::optional 中,例如 std::optional<std::variant</*...*/>>
  • std::variant 模板参数 <T1, T2, ... Tn> 中的重复项被移除;每个类型只出现一次;
  • 如果属性是 std::variant<T>std::optional<std::variant<T>>,则属性变为 Tstd::optional<T>,分别;并且
  • 如果属性是 std::variant<>std::optional<std::variant<>>,则结果变为 none
属性中容器的形成

从非容器形成容器的规则很简单。对于重复解析器,例如 +p*prepeat(3)[p] 等,您将得到一个 vector。vector 的值类型是 ATTR(p)

序列容器的另一条规则是,一个值 x 和一个包含 x 类型元素的容器 c 会形成一个单独的容器。但是,x 的类型必须与 c 中的元素完全相同。这有一个例外,就是上面提到的字符串和字符的特殊情况。例如,考虑 char_ >> string("str") 的属性。在非 Unicode 代码路径中,char_ 的属性类型保证是 char,因此 ATTR(char_ >> string("str")std::string。如果您在 Unicode 代码路径中解析 UTF-8,char_ 的属性类型是 char32_t,特殊规则也使其产生 std::string。否则,ATTR(char_ >> string("str") 的属性将是 boost::parser::tuple<char32_t, std::string>

同样,没有关于组合值和容器的特殊规则。所有组合都来自精确匹配,或者属于字符串+字符的特殊情况。

另一个特殊情况:std::string 赋值

std::string 可以从 char 赋值。这很愚蠢。但是,我们对此无能为力。当您编写具有 char 属性的解析器,并尝试将其解析为 std::string 时,您几乎肯定犯了错误。更重要的是,如果您这样写:

namespace bp = boost::parser;
std::string result;
auto b = bp::parse("3", bp::int_, bp::ws, result);

……您更有可能犯了错误。尽管这应该可行,因为 std::string s; s = 3; 中的赋值是格式正确的,但 Boost.Parser 禁止这样做。如果您编写了上述代码片段中的解析代码,您将得到一个静态断言。如果您确实想将 float 或其他类型赋值给 std::string,请在语义动作中进行。

序列和替代解析器生成的属性示例

表中:a 是语义动作;pp1p2、……是生成属性的解析器。请注意,这里只使用了 >>> 具有完全相同的属性生成规则。

表 25.10. 序列和替代组合操作及其属性

表达式

属性类型

eps >> eps

无。

p >> eps

ATTR(p)

eps >> p

ATTR(p)

cu >> string("str")

std::string

string("str") >> cu

std::string

*cu >> string("str")

boost::parser::tuple<std::string, std::string>

string("str") >> *cu

boost::parser::tuple<std::string, std::string>

p >> p

boost::parser::tuple<ATTR(p), ATTR(p)>

*p >> p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

p >> *p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

*p >> -p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

-p >> *p

std::string 如果 ATTR(p)charchar32_t,否则是 std::vector<ATTR(p)>

string("str") >> -cu

std::string

-cu >> string("str")

std::string

!p1 | p2[a]

无。

p | p

ATTR(p)

p1 | p2

std::variant<ATTR(p1), ATTR(p2)>

p | eps

std::optional<ATTR(p)>

p1 | p2 | eps

std::optional<std::variant<ATTR(p1), ATTR(p2)>>

p1 | p2[a] | p3

std::optional<std::variant<ATTR(p1), ATTR(p3)>>


merge[] 和 separate[] 详解

正如我们在上一节 解析到结构和类 中看到的,如果您连续解析两个字符串,您将在生成的属性中得到两个独立的字符串。该示例中的解析器是:

namespace bp = boost::parser;
auto employee_parser = bp::lit("employee")
    >> '{'
    >> bp::int_ >> ','
    >> quoted_string >> ','
    >> quoted_string >> ','
    >> bp::double_
    >> '}';

employee_parser 的属性是 boost::parser::tuple<int, std::string, std::string, double>。两个 quoted_string 解析器生成 std::string 属性,并且这些属性没有合并。这是默认行为,这正是我们在此情况下想要的;我们不希望第一个和最后一个名字字段粘合在一起,以至于我们无法区分一个名字何时结束,另一个名字何时开始。如果我们正在解析某个由前缀和后缀组成的字符串,并且前缀和后缀是单独定义的以便重用,会怎样?

namespace bp = boost::parser;
auto prefix = /* ... */;
auto suffix = /* ... */;
auto special_string = prefix >> suffix;
// Continue to use prefix and suffix to make other parsers....

在这种情况下,我们可能希望使用这些单独的解析器,但希望 special_string 为其属性生成一个单独的 std::stringmerge[] 就是为此目的而存在的。

namespace bp = boost::parser;
auto prefix = /* ... */;
auto suffix = /* ... */;
auto special_string = bp::merge[prefix >> suffix];

merge[] 仅适用于序列解析器(如 p1 >> p2),并强制序列解析器中的所有子解析器使用同一个变量作为其属性。

另一个指令 separate[] 也仅适用于序列解析器,但它执行的操作与 merge[] 相反。它强制序列解析器的所有子解析器生成的属性保持独立,即使它们本可以合并。例如,考虑这个解析器。

namespace bp = boost::parser;
auto string_and_char = +bp::char_('a') >> ' ' >> bp::cp;

string_and_char 匹配一个或多个 'a',然后是另一个字符。如上所示,string_and_char 生成一个 std::string,并且最后一个字符被附加到字符串中,在所有 'a' 之后。但是,如果您想将最后一个字符存储为单独的值,您可以使用 separate[]

namespace bp = boost::parser;
auto string_and_char = bp::separate[+bp::char_('a') >> ' ' >> bp::cp];

通过此更改,string_and_char 生成属性 boost::parser::tuple<std::string, char32_t>

merge[] 和 separate[] 详解

如前所述,merge[] 仅适用于序列解析器。所有子解析器必须具有相同的属性,或者根本不产生属性。至少一个子解析器必须生成属性。当您使用 merge[] 时,您创建了一个组合组。组合组中的每个解析器都使用相同的变量作为其属性。组合组中的任何解析器都不会与任何在其组合组之外的解析器的属性进行交互。组合组是独立的;merge[/*...*/] >> merge[/*...*/] 将生成两个属性的元组,而不是一个。

separate[] 也仅适用于序列解析器。当您使用 separate[] 时,您禁用了所有子解析器属性与相邻属性的交互,无论它们是在 separate[] 指令内部还是外部;您强制每个子解析器拥有一个独立的属性。

对于合并序列解析器属性的算法的步骤,merge[]separate[] 的规则会覆盖。考虑一个例子。

namespace bp = boost::parser;
constexpr auto parser =
    bp::char_ >> bp::merge[(bp::string("abc") >> bp::char_ >> bp::char_) >> bp::string("ghi")];

您可能认为 ATTR(parser) 将是 bp::tuple<char, std::string>。但事实并非如此。上述解析器甚至无法编译。由于我们在上面创建了一个合并组,我们禁用了 char_ 解析器与其前面的 string 解析器合并的默认行为。由于它们都被视为独立实体,并且它们的属性类型不同,因此使用 merge[] 是错误的。

许多指令通过使用给定的解析器来创建一个新解析器。 merge[]separate[] 则不然。由于它们仅作用于序列解析器,因此它们所做的就是创建给定序列解析器的副本。 seq_parser 模板有一个模板参数 CombiningGroups,而所有 merge[]separate[] 所做的就是获取一个给定的 seq_parser 并创建一个具有不同 CombiningGroups 模板参数的副本。这意味着 merge[]separate[]operator>> 表达式中可以像括号一样被忽略。考虑一个例子。

namespace bp = boost::parser;
constexpr auto parser1 = bp::separate[bp::int_ >> bp::int_] >> bp::int_;
constexpr auto parser2 = bp::lexeme[bp::int_ >> ' ' >> bp::int_] >> bp::int_;

请注意,separate[] 在这里是无操作的;它仅用于此示例。这些解析器具有不同的属性类型。ATTR(parser1)boost::parser::tuple(int, int, int)ATTR(parser2)boost::parser::tuple(boost::parser::tuple(int, int), int)。这是因为 bp::lexeme[] 将其给定的解析器包装在一个新解析器中。merge[] 则不会。这就是为什么,尽管 parser1parser2 的结构看起来非常相似,但它们的属性却不同。

transform(f)[]

transform(f)[] 是一个指令,它使用给定的函数 f 来转换解析器的属性。例如:

auto str_sum = [&](std::string const & s) {
    int retval = 0;
    for (auto ch : s) {
        retval += ch - '0';
    }
    return retval;
};

namespace bp = boost::parser;
constexpr auto parser = +bp::char_;
std::string str = "012345";

auto result = bp::parse(str, bp::transform(str_sum)[parser]);
assert(result);
assert(*result == 15);
static_assert(std::is_same_v<decltype(result), std::optional<int>>);

在这里,我们有一个函数 str_sum,我们将其用于 f。它假设给定的 std::string s 中的每个字符都是数字,并返回 s 中所有数字的总和。我们的解析器 parser 通常会返回一个 std::string。然而,因为 str_sum 返回一个不同的类型——int——所以 bp::transform(by_value_str_sum)[parser] 这个完整解析器的属性类型就是 int,正如 static_assert 所示。

与 Boost.Parser 中所有属性的情况一样,传递给 f 的属性将被移动。您可以按 const &&& 或按值接收。

不区分带属性和不带属性的解析器,因为存在一个由不带属性的解析器生成的常规特殊无属性类型。因此,您可以编写 transform(f)[eps] 这样的代码,Boost.Parser 会愉快地用这个特殊的无属性类型调用 f

其他影响属性生成的指令

omit[p] 禁用解析器 p 的属性生成。raw[p] 将属性从 ATTR(p) 更改为指示被 p 匹配的输入子范围的视图。string_view[p] 类似于 raw[p],但它生成 std::basic_string_view。有关详细信息,请参阅 指令

有多个顶级 parse 函数。它们有一些共同点:

  • 它们都返回一个上下文可转换为 bool 的值。
  • 它们每个都至少接受一个要解析的范围和一个解析器。 “要解析的范围”可以是迭代器/哨兵对,也可以是单个范围对象。
  • 它们每个都要求要解析的范围是可前向迭代的。
  • 它们每个都接受具有字符元素类型的任何范围。这意味着它们都可以解析 charwchar_tchar8_tchar16_tchar32_t 的范围。
  • 名称中带有 prefix_ 的重载接受迭代器/哨兵对。例如 prefix_parse(first, last, p, ws),它解析范围 [first, last),在解析过程中向前推进 first。如果解析成功,整个输入可能已被匹配,也可能未被匹配。 first 的值将指示 p 匹配的输入中的最后一个位置。当调用 parse()first == last 时,整个 输入才被匹配。
  • 当您调用任何范围重载的 parse(),例如 parse(r, p, ws) 时,parse() 仅在 r全部内容都被 p 匹配时才表示成功。
[Note] 注意

wchar_t 被接受为输入的类型。请注意,这在 MSVC 上被解释为 UTF-16,在其他所有地方被解释为 UTF-32。

重载

共有八个 parse()prefix_parse() 的重载组合,因为调用方式有三种选择。

迭代器/哨兵与范围

您可以使用分隔字符值范围的迭代器和哨兵来调用 prefix_parse()。例如:

namespace bp = boost::parser;
auto const p = /* some parser ... */;

char const * str_1 = /* ... */;
// Using null_sentinel, str_1 can point to three billion characters, and
// we can call prefix_parse() without having to find the end of the string first.
auto result_1 = bp::prefix_parse(str_1, bp::null_sentinel, p, bp::ws);

char str_2[] = /* ... */;
auto result_2 = bp::prefix_parse(std::begin(str_2), std::end(str_2), p, bp::ws);

迭代器/哨兵重载可以在不匹配整个输入的情况下成功解析。您可以通过检查 prefix_parse() 返回后 first == last 是否为真来判断整个输入是否被匹配。

相比之下,您使用字符值范围来调用 parse()。当范围是字符数组的引用时,任何终止的 0 都会被忽略;这使得像 parse("str", p) 这样的调用能够自然工作。

namespace bp = boost::parser;
auto const p = /* some parser ... */;

std::u8string str_1 = "str";
auto result_1 = bp::parse(str_1, p, bp::ws);

// The null terminator is ignored.  This call parses s-t-r, not s-t-r-0.
auto result_2 = bp::parse(U"str", p, bp::ws);

char const * str_3 = "str";
auto result_3 = bp::parse(bp::null_term(str_3) | bp::as_utf16, p, bp::ws);

由于无法表明 p 匹配了输入,但只匹配了输入的前缀,因此 parse() 的范围(非迭代器/哨兵)重载如果在整个输入未被匹配时指示失败。

带或不带属性输出参数
namespace bp = boost::parser;
auto const p = '"' >> *(bp::char_ - '"') >> '"';
char const * str = "\"two words\"" ;

std::string result_1;
bool const success = bp::parse(str, p, result_1);   // success is true; result_1 is "two words"
auto result_2 = bp::parse(str, p);                  // !!result_2 is true; *result_2 is "two words"

当您调用 parse() 属性输出参数和解析器 p 时,期望类型是 类似于 ATTR(p)。它不必完全如此;我稍后会解释。返回类型是 bool

当您调用 parse() 不带 属性输出参数和解析器 p 时,返回类型是 std::optional<ATTR(p)>。请注意,当 ATTR(p) 本身是一个 optional 时,返回类型是 std::optional<std::optional<...>>。每个 optional 都表示不同的信息。外部 optional 表示解析是否成功。如果成功,解析器成功了,但它仍然生成一个 optional 属性——这就是内部的。

带或不带跳过符
namespace bp = boost::parser;
auto const p = '"' >> *(bp::char_ - '"') >> '"';
char const * str = "\"two words\"" ;

auto result_1 = bp::parse(str, p);         // !!result_1 is true; *result_1 is "two words"
auto result_2 = bp::parse(str, p, bp::ws); // !!result_2 is true; *result_2 is "twowords"
属性输出参数的兼容性

对于任何接受属性输出参数的 parse() 调用,例如 parse("str", p, bp::ws, out),调用对于 out 的多种可能类型都是格式正确的;decltype(out) 不必正好是 ATTR(p)

例如,以下是格式正确的代码,不会中止(请记住 string() 的属性类型是 std::string):

namespace bp = boost::parser;
auto const p = bp::string("foo");

std::vector<char> result;
bool const success = bp::parse("foo", p, result);
assert(success && result == std::vector<char>({'f', 'o', 'o'}));

即使 p 生成 std::string 属性,当它实际获取生成的数据并将其写入属性时,它只假设属性是 container(参见 Concepts),而不是它是什么特定容器类型。它仍然可以愉快地 insert()std::stringstd::vector<char> 中。std::stringstd::vector<char> 都是 char 的容器,但它也可以插入到具有不同元素类型的容器中。p 只需能够将其产生的元素插入到属性容器中。只要隐式转换允许其正常工作,一切就都没问题。

namespace bp = boost::parser;
auto const p = bp::string("foo");

std::deque<int> result;
bool const success = bp::parse("foo", p, result);
assert(success && result == std::deque<int>({'f', 'o', 'o'}));

这也可以工作,即使这需要将来自生成的 char32_t 序列的元素插入到 char 的容器中(记住 +cp 的属性类型是 std::vector<char32_t>)。

namespace bp = boost::parser;
auto const p = +bp::cp;

std::string result;
bool const success = bp::parse("foo", p, result);
assert(success && result == "foo");

下一个示例也有效,即使容器的更改不是顶层的。它是结果元组的一个元素。

namespace bp = boost::parser;
auto const p = +(bp::cp - ' ') >> ' ' >> string("foo");

using attr_type = decltype(bp::parse(u8"", p));
static_assert(std::is_same_v<
              attr_type,
              std::optional<bp::tuple<std::string, std::string>>>);

using namespace bp::literals;

{
    // This is similar to attr_type, with the first std::string changed to a std::vector<int>.
    bp::tuple<std::vector<int>, std::string> result;
    bool const success = bp::parse(u8"rôle foo" | bp::as_utf8, p, result);
    assert(success);
    assert(bp::get(result, 0_c) == std::vector<int>({'r', U'ô', 'l', 'e'}));
    assert(bp::get(result, 1_c) == "foo");
}
{
    // This time, we have a std::vector<char> instead of a std::vector<int>.
    bp::tuple<std::vector<char>, std::string> result;
    bool const success = bp::parse(u8"rôle foo" | bp::as_utf8, p, result);
    assert(success);
    // The 4 code points "rôle" get transcoded to 5 UTF-8 code points to fit in the std::string.
    assert(bp::get(result, 0_c) == std::vector<char>({'r', (char)0xc3, (char)0xb4, 'l', 'e'}));
    assert(bp::get(result, 1_c) == "foo");
}

如内联注释所示,此示例有几点需要注意:

  • 如果您更改属性输出参数(例如,将 std::string 改为 std::vector<int>,或将 std::vector<char32_t> 改为 std::deque<int>),parse() 调用通常仍然是格式正确的。
  • 更改容器类型时,如果两个容器都包含字符值,则移除的容器的元素类型是 char32_t(或非 MSVC 构建的 wchar_t),新容器的元素类型是 charchar8_t,Boost.Parser 假定这是一个 UTF-32 到 UTF-8 的转换,并在插入到新容器时静默转码数据。

让我们看一个其他看似简单的类型替换不起作用的案例。首先,起作用的案例:

namespace bp = boost::parser;
auto parser = -(bp::char_ % ',');
std::vector<int> result;
auto b = bp::parse("a, b", parser, bp::ws, result);

ATTR(parser)std::optional<std::string>。尽管我们传递了一个 std::vector<int>,但一切都很顺利。然而,如果我们稍微修改这个案例,让 std::optional<std::string> 嵌套在属性中,代码将变得格式不正确。

struct S
{
    std::vector<int> chars;
    int i;
};
namespace bp = boost::parser;
auto parser = -(bp::char_ % ',') >> bp::int_;
S result;
auto b = bp::parse("a, b 42", parser, bp::ws, result);

如果我们更改 charsstd::vector<char>,代码仍然格式不正确。如果我们更改 charsstd::string,结果也一样。我们必须实际使用 std::optional<std::string> 才能使代码重新格式正确。

顶级解析器中存在的相同宽松性不适用于嵌套解析器,是因为在代码的某个点,解析器 -(bp::char_ % ',') 会尝试将 std::optional<std::string>(它通常生成的属性类型的元素类型)赋给一个 chars。如果那里没有隐式转换,代码就是格式错误的。

最后一个示例的要点是,在传递给 parse() 的属性类型中任意替换数据类型的能力非常灵活,但也仅限于结构简单的场景。在下一节讨论 规则 时,我们将看到属性类型的这种灵活性如何帮助编写复杂的解析器。

这些是用于将一种容器类型替换为另一种容器类型的示例。它们是很好的示例,因为它们更容易令人惊讶,因此在这里得到了广泛的介绍。您还可以做更简单的事情,例如使用 uint_ 进行解析,并将其属性写入 double。一般来说,只要替换不会导致解析过程中出现格式错误的赋值,您就可以将任何类型 T 从属性中替换出来。

这是另一个因不同原因产生意外结果的示例。

namespace bp = boost::parser;
constexpr auto parser = bp::char_('a') >> bp::char_('b') >> bp::char_('c') |
                        bp::char_('x') >> bp::char_('y') >> bp::char_('z');
std::string str = "abc";
bp::tuple<char, char, char> chars;
bool b = bp::parse(str, parser, chars);
assert(b);
assert(chars == bp::tuple('c', '\0', '\0'));

这看起来是错误的,但属于预期行为。在生成属性的解析的每个阶段,如果提供了输出参数属性,Boost.Parser 会尝试将其赋给 parse() 的输出参数属性的某个部分。请注意,ATTR(parser)std::string,因为每个序列解析器是三个连续的 char_ 解析器,它们构成一个 std::string;有两个这样的替代项,因此总体属性也是 std::string。在解析过程中,当第一个解析器 bp::char_('a') 匹配输入时,它会生成属性 'a' 并需要将其赋给其目标。序列解析器内部的一些逻辑表明,这个 'a' 贡献了结果元组第 0 个位置的值,如果结果被写入元组的话。在这里,我们传递了一个 bp::tuple<char, char, char>,所以它将 'a' 写入第一个元素。每个后续的 char_ 解析器都会执行相同的操作,并覆盖第一个元素。如果我们传递一个 std::string 作为输出参数,逻辑会发现输出参数属性是字符串,然后会将其 'a' 追加到其后。然后每个后续解析器都会追加到字符串。

Boost.Parser 永远不会查看传递给 parse() 的元组的基数,以查看其中是否有太多或太少的元素,与解析器的预期属性相比。在这种情况下,有两个额外的元素永远不会被触及。如果元组中的元素太少,您就会收到编译错误。Boost.Parser 从不进行这种类型检查的原因是,松散的赋值逻辑分布在各个解析器中;顶级解析可以确定预期的属性是什么,但不能确定传递的另一个类型的属性是否是合适的替代项。

variant 属性输出参数的兼容性

如果默认属性可以赋给 variant,则输出参数中的 variant 使用是兼容的。没有其他工作来使赋值兼容。例如,这会按您预期的方式工作

namespace bp = boost::parser;
std::variant<int, double> v;
auto b = bp::parse("42", bp::int_, v);
assert(b);
assert(v.index() == 0);
assert(std::get<0>(v) == 42);

同样,这之所以可行,是因为 v = 42 是格式良好的。但是,其他类型的替换将不起作用。特别是,从 boost::parser::tuple 到聚合或从聚合到 boost::parser::tuple 的转换将不起作用。这是一个例子。

struct key_value
{
    int key;
    double value;
};

namespace bp = boost::parser;
std::variant<key_value, double> kv_or_d;
key_value kv;
bp::parse("42 13.0", bp::int_ >> bp::double_, kv);      // Ok.
bp::parse("42 13.0", bp::int_ >> bp::double_, kv_or_d); // Error: ill-formed!

在这种情况下,Boost.Parser 可以很容易地查看 variant 覆盖的替代类型,并进行转换。但是,在许多情况下,没有明显正确的 variant 替代类型,或者用户可能会期望一种 variant 替代类型,但得到另一种。考虑几个案例。

struct i_d { int i; double d; };
struct d_i { double d; int i; };
using v1 = std::variant<i_d, d_i>;

struct i_s { int i; short s; };
struct d_d { double d1; double d2; };
using v2 = std::variant<i_s, d_d>;

using tup_t = boost::parser::tuple<short, short>;

如果我们有一个生成 tup_t 的解析器,并且我们有一个 v1 属性输出参数,则正确的 variant 替代类型明显不存在——这种情况是模棱两可的,任何人都可以看出没有一个 variant 替代项是更好的匹配。如果我们正在将 tup_t 赋给 v2,那就更糟了。模棱两可的情况相同,但对用户而言,i_s 显然比 d_d 更“接近”。

因此,Boost.Parser 只执行赋值。如果某个解析器 P 生成的默认属性无法赋给您想为其赋值的 variant 替代项,您可以创建一个 规则,该规则创建精确的 variant 替代类型或 variant 本身,并将 P 用作您的规则的解析器。

Unicode 与非 Unicode 解析

调用 parse() 要么将整个输入视为 UTF 格式(UTF-8、UTF-16 或 UTF-32),要么将其视为某种未知编码。以下是它如何推断调用属于哪种情况

  • 如果范围是 char8_t 的序列,或者输入是 boost::parser::utf8_view,则输入为 UTF-8。
  • 否则,如果范围的值类型是 char,则输入处于未知编码。
  • 否则,输入为 UTF 编码。
[Tip] 提示

如果您想以仅 ASCII 模式或某种其他非 Unicode 编码进行解析,请仅使用 char 的序列,例如 std::stringchar const *

[Tip] 提示

如果您想确保所有输入都作为 Unicode 进行解析,请将输入范围 r 传递为 r | boost::parser::as_utf32——这在 Unicode 解析路径中,在 parse() 内部,这是它首先要做的事情。

[Note] 注意

由于传递 boost::parser::utfN_view 是一个特例,并且 char 的序列 r 否则被视为未知编码,因此 boost::parser::parse(r | boost::parser::as_utf8, p)r 视为 UTF-8,而 boost::parser::parse(r, p) 则不。

parse() 的 trace_mode 参数

一旦解析器达到一定大小,调试解析器就非常困难。要获取详细的解析跟踪,请将 boost::parser::trace::on 作为最后一个参数传递给 parse()。它将显示当前匹配的解析器、接下来的几个待解析的字符以及生成的任何属性。有关详细信息,请参阅教程的错误处理和调试部分。

全局变量和错误处理程序

每次调用 parse() 时,都可以选择关联一个全局变量对象。要将特定的全局变量对象与您的解析器一起使用,请调用 with_globals() 来创建一个包含全局变量对象的新解析器

struct globals_t
{
    int foo;
    std::string bar;
};
auto const parser = /* ... */;
globals_t globals{42, "yay"};
auto result = boost::parser::parse("str", boost::parser::with_globals(parser, globals));

该调用中的每个语义动作 parse() 都可以使用 _globals(ctx) 访问同一个 globals_t 对象。

默认错误处理程序对大多数需求来说都很好,但如果您想更改它,可以通过调用 with_error_handler() 来创建一个新的解析器来完成。

auto const parser = /* ... */;
my_error_handler error_handler;
auto result = boost::parser::parse("str", boost::parser::with_error_handler(parser, error_handler));
[Tip] 提示

如果您的解析环境不允许您将错误报告到终端,您可能希望使用 callback_error_handler 而不是默认错误处理程序。

[Important] 重要提示

全局变量和错误处理程序(如果存在)会被忽略,除非它们应用于顶级解析器。

在早期关于 规则 的页面(规则解析器)中,我将 规则 描述为类似于函数。 规则 本质上是组织性的。以下是 规则 的常见用例。如果您想:

  • 将解析器生成的属性类型固定为默认类型以外的类型;
  • 控制相邻序列解析器生成的属性;
  • 创建生成有用诊断文本的解析器;
  • 创建递归规则(下文详述);
  • 创建一组相互递归的解析器;
  • 执行回调解析。

让我们详细看看这些用例。

固定属性类型

我们在上一节中看到了 parse() 在接受什么类型的属性输出参数方面有多灵活。再看一个例子。

namespace bp = boost::parser;
auto result = bp::parse(input, bp::int % ',', result);

result 可以是许多不同类型之一。它可以是 std::vector<int>。它可以是 std::set<long long>。它可以是很多东西。通常,这是一个非常有用的属性;如果您必须重写所有解析器逻辑,因为您更改了属性某一部分中所需的容器,从 std::vector 改为 std::deque,那将是令人沮丧的。然而,这种灵活性是以类型检查为代价的。如果您想编写一个 始终 只生成 std::vector<unsigned int> 而不是其他任何类型 的解析器,您也可能希望在意外地将该解析器传递 std::set<unsigned int> 属性而不是时收到编译错误。没有办法通过普通解析器来强制其属性类型只能是单个固定类型。

幸运的是,规则 允许您编写具有固定属性类型的解析器。每个规则都有一个特定的属性类型,作为模板参数提供。如果未指定,则规则没有属性。属性是特定类型的事实允许您消除属性的灵活性。例如,假设我们有一个规则定义如下

bp::rule<struct doubles, std::vector<double>> doubles = "doubles";
auto const doubles_def = bp::double_ % ',';
BOOST_PARSER_DEFINE_RULES(doubles);

然后您可以在调用 parse() 中使用它,并且 parse() 将返回一个 std::optional<std::vector<double>>

auto const result = bp::parse(input, doubles, bp::ws);

如果您调用 parse() 并带有属性输出参数,它必须精确为 std::vector<double>

std::vector<double> vec_result;
bp::parse(input, doubles, bp::ws, vec_result); // Ok.
std::deque<double> deque_result;
bp::parse(input, doubles, bp::ws, deque_result); // Ill-formed!

如果我们想将 std::deque<double> 用作我们规则的属性类型

// Attribute changed to std::deque<double>.
bp::rule<struct doubles, std::deque<double>> doubles = "doubles";
auto const doubles_def = bp::double_ % ',';
BOOST_PARSER_DEFINE_RULES(doubles);

int main()
{
    std::deque<double> deque_result;
    bp::parse(input, doubles, bp::ws, deque_result); // Ok.
}

这里的要点是,属性的灵活性仍然可用,但仅 规则内部——解析器 bp::double_ % ',' 可以解析为 std::vector<double>std::deque<double>,但规则 doubles 必须仅解析为它声明要生成的精确属性。

原因是,在规则解析实现内部,有类似这样的代码

using attr_t = ATTR(doubles_def);
attr_t attr;
parse(first, last, parser, attr);
attribute_out_param = std::move(attr);

其中 attribute_out_param 是我们传递给 parse() 的属性输出参数。如果最终的移动赋值格式错误,则调用 parse() 也格式错误。

您还可以使用规则来利用属性的灵活性。尽管规则减少了它可以生成的属性的灵活性,但编写新规则如此容易的事实意味着我们可以使用规则本身来获得我们代码所需的属性灵活性。

namespace bp = boost::parser;

// We only need to write the definition once...
auto const generic_doubles_def = bp::double_ % ',';

bp::rule<struct vec_doubles, std::vector<double>> vec_doubles = "vec_doubles";
auto const & vec_doubles_def = generic_doubles_def; // ... and re-use it,
BOOST_PARSER_DEFINE_RULES(vec_doubles);

// Attribute changed to std::deque<double>.
bp::rule<struct deque_doubles, std::deque<double>> deque_doubles = "deque_doubles";
auto const & deque_doubles_def = generic_doubles_def; // ... and re-use it again.
BOOST_PARSER_DEFINE_RULES(deque_doubles);

现在我们各有一个,而且我们不必复制代码逻辑,这些逻辑必须在两个地方维护。

有时,您需要创建一个规则来强制执行某种属性类型,但该规则的属性无法从其解析器的属性构造。在这种情况下,您需要编写一个语义动作。

struct type_t
{
    type_t() = default;
    explicit type_t(double x) : x_(x) {}
    // etc.

    double x_;
};

namespace bp = boost::parser;

auto doubles_to_type = [](auto & ctx) {
    using namespace bp::literals;
    _val(ctx) = type_t(_attr(ctx)[0_c] * _attr(ctx)[1_c]);
};

bp::rule<struct type_tag, type_t> type = "type";
auto const type_def = (bp::double_ >> bp::double_)[doubles_to_type];
BOOST_PARSER_DEFINE_RULES(type);

对于规则 R 及其解析器 P,我们不需要编写这样的语义动作,如果

- ATTR(R) 是一个聚合,并且 ATTR(P) 是一个兼容的元组;

- ATTR(R) 是一个元组,并且 ATTR(P) 是一个兼容的聚合;

- ATTR(R) 是一个非聚合类类型 C,并且 ATTR(P) 是一个可以使用其元素来构造 C 的元组;或

- ATTR(R)ATTR(P) 是兼容的类型。

"兼容" 的概念在 parse() API 中定义。

控制生成的属性

有关详细信息,请参阅属性生成部分中的序列解析器属性示例

创建用于更好诊断的解析器

每个 规则 都有关联的诊断文本,Boost.Parser 可将其用于该规则的失败。当解析在期望点(请参阅 期望点)处发生解析失败时,这很有用。假设您在某处定义了以下代码。

namespace bp = boost::parser;

bp::rule<struct value_tag> value =
    "an integer, or a list of integers in braces";

auto const ints = '{' > (value % ',') > '}';
auto const value_def = bp::int_ | ints;

BOOST_PARSER_DEFINE_RULES(value);

注意两个期望点。一个在 (value % ',') 之前,一个在最后的 '}' 之前。稍后,您在某个输入中调用 parse

bp::parse("{ 4, 5 a", value, bp::ws);

这会触发第二个期望点,并产生类似如下的输出

1:7: error: Expected '}' here:
{ 4, 5 a
       ^

这是一个很好的错误消息。如果我们违反了之前的期望,它看起来是这样的

bp::parse("{ }", value, bp::ws);
1:2: error: Expected an integer, or a list of integers in braces % ',' here:
{ }
  ^

没那么好。问题是期望点在 (value % ',') 上。所以,即使我们给 value 提供了合理的诊断文本,我们也把它放在了错误的地方。我们可以引入一个新规则来将诊断文本放在正确的位置。

namespace bp = boost::parser;

bp::rule<struct value_tag> value =
    "an integer, or a list of integers in braces";
bp::rule<struct comma_values_tag> comma_values =
    "a comma-delimited list of integers";

auto const ints = '{' > comma_values > '}';
auto const value_def = bp::int_ | ints;
auto const comma_values_def = (value % ',');

BOOST_PARSER_DEFINE_RULES(value, comma_values);

现在当我们调用 bp::parse("{ }", value, bp::ws) 时,我们会得到一个更好的消息

1:2: error: Expected a comma-delimited list of integers here:
{ }
  ^

规则 value 可能在我们的代码的其他地方有用,也许在另一个解析器中。它的诊断文本适用于其他潜在用途。

递归规则

在语法中包含递归规则是很常见的。考虑用于平衡括号的这个 EBNF 规则

<parens> ::= "" | ( "(" <parens> ")" )

我们可以尝试使用 Boost.Parser 这样写

namespace bp = boost::parser;
auto const parens = '(' >> parens >> ')' | bp::eps;

我们将 bp::eps 放在第二个,因为 Boost.Parser 的解析算法是贪婪的。否则,这只是一个直接的转写。不幸的是,它不起作用。代码是格式错误的,因为您无法定义一个变量来定义它本身。好吧,你可以,但结果不佳。如果我们用一个前向声明的 规则 来定义解析器,它就会起作用。

namespace bp = boost::parser;
bp::rule<struct parens_tag> parens = "matched parentheses";
auto const parens_def = '(' >> parens > ')' | bp::eps;
BOOST_PARSER_DEFINE_RULES(parens);

稍后,如果我们用它来解析,它就会按我们想要的方式进行。

assert(bp::parse("(((())))", parens, bp::ws));

当它失败时,它甚至会产生很好的诊断。

bp::parse("(((()))", parens, bp::ws);
1:7: error: Expected ')' here (end of input):
(((()))
       ^

递归 规则 在一个方面与其他解析器不同:在递归地重新进入规则时,只有属性变量(在语义动作中是 _attr(ctx))对于该规则的该实例是唯一的。该规则的最上层实例的所有其他状态都是共享的。这包括规则的值(_val(ctx)),以及规则的局部变量和参数。换句话说,_val(ctx) 在递归 规则 的每个实例中都返回对 同一对象 的引用。这是因为规则的每个实例都需要一个地方来存储它从解析中生成的属性。然而,我们只希望最上层规则有一个返回值;如果每个实例在 _val(ctx) 中都有单独的值,那么在递归实例的求值过程中,就不可能逐步构建递归规则的结果。

另外,考虑这个规则

namespace bp = boost::parser;
bp::rule<struct ints_tag, std::vector<int>> ints = "ints";
auto const ints_def = bp::int_ >> ints | bp::eps;

ints_def 的默认属性类型是什么?它看起来确实是 std::optional<std::vector<int>>。在评估 ints 的过程中,Boost.Parser 必须评估 ints_def,然后从中生成一个 std::vector<int>——这是 ints 的返回类型——。怎么做?如何将 std::optional<std::vector<int>> 转换为 std::vector<int>?对人来说,这似乎显而易见,但能够正确处理这个简单示例和一般情况的元编程肯定超出了我的能力范围。

Boost.Parser 对什么构成递归规则有特定的语义。每个规则都有一个关联的标签类型,如果 Boost.Parser 进入一个具有特定标签 Tag 的规则,并且当前正在评估的规则(如果存在)也具有标签 Tag,那么进入的规则实例将被视为递归。没有其他情况被视为递归。特别是,如果您有规则 RaRb,并且 Ra 使用了 Rb,而 Rb 又使用了 Ra,那么第二次使用 Ra 不被视为递归。 RaRb 当然是相互递归的,但为了获得唯一值、局部变量和参数,两者都不被视为“递归规则”。

相互递归规则

使用规则的一个优点是,您可以预先声明所有规则,然后在之后立即使用它们。这允许您创建相互使用的规则,而不会引入循环。

namespace bp = boost::parser;

// Assume we have some polymorphic type that can be an object/dictionary,
// array, string, or int, called `value_type`.

bp::rule<class string, std::string> const string = "string";
bp::rule<class object_element, bp::tuple<std::string, value_type>> const object_element = "object-element";
bp::rule<class object, value_type> const object = "object";
bp::rule<class array, value_type> const array = "array";
bp::rule<class value_tag, value_type> const value = "value";

auto const string_def = bp::lexeme['"' >> *(bp::char_ - '"') > '"'];
auto const object_element_def = string > ':' > value;
auto const object_def = '{'_l >> -(object_element % ',') > '}';
auto const array_def = '['_l >> -(value % ',') > ']';
auto const value_def = bp::int_ | bp::bool_ | string | array | object;

BOOST_PARSER_DEFINE_RULES(string, object_element, object, array, value);

在这里,我们有一个类似 Javascript 的值类型 value_type 的解析器。 value_type 可以是一个数组,它本身可能包含其他数组、对象、字符串等。由于我们需要能够解析数组中的对象,反之亦然,因此我们需要这两个解析器都能相互引用。

回调解析

只有 规则 才能成为回调解析器,所以如果您想通过回调而不是在表示整个解析结果的巨大属性的中间位置获取属性,您就需要使用 规则。有关回调解析的详细示例,请参阅使用回调解析 JSON

规则上的语义动作中可用的访问器
_val()

在规则的所有语义动作中,表达式 _val(ctx) 是对规则生成的属性的引用。当您希望子解析器以特定方式构建属性时,这可能很有用。

namespace bp = boost::parser;
using namespace bp::literals;

bp::rule<class ints, std::vector<int>> const ints = "ints";
auto twenty_zeros = [](auto & ctx) { _val(ctx).resize(20, 0); };
auto push_back = [](auto & ctx) { _val(ctx).push_back(_attr(ctx)); };
auto const ints_def = "20-zeros"_l[twenty_zeros] | +bp::int_[push_back];
BOOST_PARSER_DEFINE_RULES(ints);
[Tip] 提示

这只是一个示例。几乎总最好避免使用语义动作。我们也可以写 ints_def"20-zeros" >> bp::attr(std::vector<int>(20)) | +bp::int_,它具有相同的语义,更容易阅读,代码也少得多。

局部变量

rule 模板接受我们尚未讨论的另一个模板参数。您可以将第三个参数 LocalState 传递给 rule,它将由 rule 默认构造,并在规则中使用的语义动作中通过 _locals(ctx) 提供。这为您的规则提供了一些局部状态,如果它需要的话。 LocalState 的类型可以是任何常规类型。它可以是单个值、包含多个值的结构,或者元组等。

struct foo_locals
{
    char first_value = 0;
};

namespace bp = boost::parser;

bp::rule<class foo, int, foo_locals> const foo = "foo";

auto record_first = [](auto & ctx) { _locals(ctx).first_value = _attr(ctx); }
auto check_against_first = [](auto & ctx) {
    char const first = _locals(ctx).first_value;
    char const attr = _attr(ctx);
    if (attr == first)
        _pass(ctx) = false;
    _val(ctx) = (int(first) << 8) | int(attr);
};

auto const foo_def = bp::cu[record_first] >> bp::cu[check_against_first];
BOOST_PARSER_DEFINE_RULES(foo);

foo 匹配输入,如果它可以连续匹配输入中的两个元素,但前提是它们的值不相同。没有局部变量,编写需要跟踪状态的解析器会更困难。

参数

有时,参数化解析器会很方便。考虑来自 YAML 1.2 规范的这些解析规则

[80]
s-separate(n,BLOCK-OUT) ::= s-separate-lines(n)
s-separate(n,BLOCK-IN)  ::= s-separate-lines(n)
s-separate(n,FLOW-OUT)  ::= s-separate-lines(n)
s-separate(n,FLOW-IN)   ::= s-separate-lines(n)
s-separate(n,BLOCK-KEY) ::= s-separate-in-line
s-separate(n,FLOW-KEY)  ::= s-separate-in-line

[136]
in-flow(n,FLOW-OUT)  ::= ns-s-flow-seq-entries(n,FLOW-IN)
in-flow(n,FLOW-IN)   ::= ns-s-flow-seq-entries(n,FLOW-IN)
in-flow(n,BLOCK-KEY) ::= ns-s-flow-seq-entries(n,FLOW-KEY)
in-flow(n,FLOW-KEY)  ::= ns-s-flow-seq-entries(n,FLOW-KEY)

[137]
c-flow-sequence(n,c) ::= “[” s-separate(n,c)? in-flow(c)? “]”

YAML [137] 说解析应该进入两个 YAML 子规则,它们都具有这些 nc 参数。将这些 YAML 解析规则转换为使用非参数化 Boost.Parser 规则 的内容是可能的,但这样做非常痛苦。最好使用参数化规则。

您通过调用规则的 with() 成员来向 规则 传递参数。传递给 with() 的值用于创建一个 boost::parser::tuple,该元组在附加到规则的语义动作中,通过 _params(ctx) 可访问。

以这种方式向 规则 传递参数允许您轻松编写根据已解析的上下文数据而改变解析方式的解析器。

这是 YAML [137] 的实现。它还实现了 [137] 直接使用的两个 YAML 规则,即规则 [136] 和 [80]。这些规则使用的规则也在此处表示,但仅使用 eps 实现,以便我不必重复(非常庞大)的 YAML 规范。

namespace bp = boost::parser;

// A type to represent the YAML parse context.
enum class context {
    block_in,
    block_out,
    block_key,
    flow_in,
    flow_out,
    flow_key
};

// A YAML value; no need to fill it in for this example.
struct value
{
    // ...
};

// YAML [66], just stubbed in here.
auto const s_separate_in_line = bp::eps;

// YAML [137].
bp::rule<struct c_flow_seq_tag, value> c_flow_sequence = "c-flow-sequence";
// YAML [80].
bp::rule<struct s_separate_tag> s_separate = "s-separate";
// YAML [136].
bp::rule<struct in_flow_tag, value> in_flow = "in-flow";
// YAML [138]; just eps below.
bp::rule<struct ns_s_flow_seq_entries_tag, value> ns_s_flow_seq_entries =
    "ns-s-flow-seq-entries";
// YAML [81]; just eps below.
bp::rule<struct s_separate_lines_tag> s_separate_lines = "s-separate-lines";

// Parser for YAML [137].
auto const c_flow_sequence_def =
    '[' >>
    -s_separate.with(bp::_p<0>, bp::_p<1>) >>
    -in_flow.with(bp::_p<0>, bp::_p<1>) >>
    ']';
// Parser for YAML [80].
auto const s_separate_def = bp::switch_(bp::_p<1>)
    (context::block_out, s_separate_lines.with(bp::_p<0>))
    (context::block_in, s_separate_lines.with(bp::_p<0>))
    (context::flow_out, s_separate_lines.with(bp::_p<0>))
    (context::flow_in, s_separate_lines.with(bp::_p<0>))
    (context::block_key, s_separate_in_line)
    (context::flow_key, s_separate_in_line);
// Parser for YAML [136].
auto const in_flow_def = bp::switch_(bp::_p<1>)
    (context::flow_out, ns_s_flow_seq_entries.with(bp::_p<0>, context::flow_in))
    (context::flow_in, ns_s_flow_seq_entries.with(bp::_p<0>, context::flow_in))
    (context::block_out, ns_s_flow_seq_entries.with(bp::_p<0>, context::flow_key))
    (context::flow_key, ns_s_flow_seq_entries.with(bp::_p<0>, context::flow_key));

auto const ns_s_flow_seq_entries_def = bp::eps;
auto const s_separate_lines_def = bp::eps;

BOOST_PARSER_DEFINE_RULES(
    c_flow_sequence,
    s_separate,
    in_flow,
    ns_s_flow_seq_entries,
    s_separate_lines);

YAML [137](c_flow_sequence)解析列表。列表可能为空,并且必须用括号括起来,如您在此处所见。但是,根据当前的 YAML 上下文([137] 的 c 参数),我们可能要求 s-separate 匹配特定的空格,并且子解析器 in-flow 的行为也取决于当前上下文。

在上面的 s_separate 中,我们根据第二个参数 c 的值以不同的方式解析。这是通过在 switch-parser 中使用 s_separate 的第二个参数的值来完成的。第二个参数是通过使用 _p 作为解析参数来查找的。

in_flow 做类似的事情。请注意,in_flow 通过传递其第一个参数来调用其子规则,但为第二个值使用固定值。 s_separate 仅有条件地传递其 n 参数。关键在于,一个规则可以与和不带 .with() 一起使用,并且您可以将常量或解析参数传递给 .with()

定义了这些规则后,我们可以为 YAML [137] 编写一个单元测试,如下所示

auto const test_parser = c_flow_sequence.with(4, context::block_out);
auto result = bp::parse("[]", test_parser);
assert(result);

您可以为 nc 的不同值扩展此测试。显然,在实际测试中,您会解析 "[]" 中的实际内容,如果其他规则已实现,例如 [138]。

_p 变量模板

获取规则的一个参数并将其作为另一个解析器的参数传递可能会非常冗长。 _p 是一个变量模板,它允许您引用当前规则的第 n 个参数,以便您可以将其传递给规则的子解析器之一。使用此功能,上面的 foo_def 可以重写为

auto const foo_def = bp::repeat(bp::_p<0>)[' '_l];

使用 _p 可以避免您编写大量 lambda 函数,这些函数通过 _params(ctx)[0_c] 或类似的方式从解析上下文中提取参数。

请注意,_p 是一个解析参数(参见 解析器及其用途),这意味着它是一个可调用对象,仅以上下文作为其参数。如果您想在语义动作中使用它,您必须调用它。

规则内部可用的特殊形式的语义动作

本教程中的语义动作通常具有签名 void (auto & ctx)。也就是说,它们按引用接受上下文,并且不返回任何内容。如果它们返回某些内容,该内容将被丢弃。

创建一个规则以从解析器中获取某种类型的值,而通常情况下您不会自动获得它,这是一个非常常见的模式。如果我想解析一个 intint_ 可以做到这一点,并且我解析的内容也是所需的属性。如果我解析一个 int 后跟一个 double,我会得到一个包含两者之一的 boost::parser::tuple。但是,如果我不需要这两个值,而是需要这两个值的一些函数呢?我可能会这样写。

struct obj_t { /* ... */ };
obj_t to_obj(int i, double d) { /* ... */ }

namespace bp = boost::parser;
bp::rule<struct obj_tag, obj_t> obj = "obj";
auto make_obj = [](auto & ctx) {
    using boost::hana::literals;
    _val(ctx) = to_obj(_attr(ctx)[0_c], _attr(ctx)[1_c]);
};
constexpr auto obj_def = (bp::int_ >> bp::double_)[make_obj];

这没关系,虽然有点冗长。但是,您也可以这样做

namespace bp = boost::parser;
bp::rule<struct obj_tag, obj_t> obj = "obj";
auto make_obj = [](auto & ctx) {
    using boost::hana::literals;
    return to_obj(_attr(ctx)[0_c], _attr(ctx)[1_c]);
};
constexpr auto obj_def = (bp::int_ >> bp::double_)[make_obj];

上面,我们从语义动作中返回值,返回的值被赋给 _val(ctx)

最后,您可以提供一个函数,该函数接受属性的单个元素(如果它是元组),并返回要赋给 _val(ctx) 的值。

namespace bp = boost::parser;
bp::rule<struct obj_tag, obj_t> obj = "obj";
constexpr auto obj_def = (bp::int_ >> bp::double_)[to_obj];

更正式地说,在规则内部,语义动作的使用确定如下。假设我们有一个函数 APPLY,它调用一个函数,参数是元组的元素,类似于 std::apply。对于某个上下文 ctx、语义动作 action 和属性 attraction 的使用方式如下

- _val(ctx) = APPLY(action, std::move(attr)),如果格式良好,并且 attr 是大小为 2 或更大的元组;

- 否则,_val(ctx) = action(ctx),如果格式良好;

- 否则,action(ctx)

第一种情况根本不将上下文传递给动作。最后一种情况是在规则外部使用语义动作的常规用法。

除非另有说明,否则所有算法和视图的约束与 parse() 重载的约束非常相似。它们接受的范围、解析器等类型相同。

boost::parser::search()

parse() API 中所示,Boost.Parser 中的两种解析模式是整解析和前缀解析。当您想在正在解析的范围中间查找内容时,没有用于此的 parse API。当然,您可以编写一个简单的解析器来跳过您正在查找的内容之前的所有内容。

namespace bp = boost::parser;
constexpr auto parser = /* ... */;
constexpr auto middle_parser = bp::omit[*(bp::char_ - parser)] >> parser;

middle_parser 将逐个 char_ 跳过所有内容,只要下一个 char_ 不是成功匹配 parser 的开始。在此之后,控制权将传递给 parser 本身。好吧,这并不难写。如果您需要解析中间的某些内容以生成属性,那么您应该使用此方法。

然而,事实证明,您通常只需要在解析范围中查找某个子范围。在这些情况下,将其变成类似 std::ranges 中算法的正确算法会很好,因为这样更符合习惯。 boost::parser::search() 就是该算法。它具有与 std::ranges::search 非常相似的语义,不同之处在于它搜索的不是精确子范围的匹配,而是给定解析器的匹配。像 std::ranges::search() 一样,它返回一个子范围(C++17 中的 boost::parser::subrange,C++20 及更高版本中的 std::ranges::subrange)。

namespace bp = boost::parser;
auto result = bp::search("aaXYZq", bp::lit("XYZ"), bp::ws);
assert(!result.empty());
assert(std::string_view(result.begin(), result.end() - result.begin()) == "XYZ");

由于 boost::parser::search() 返回一个子范围,您提供的任何解析器都不会生成属性。我在上面写了 bp::lit("XYZ");如果我改为写 bp::string("XYZ"),结果(以及缺少 std::string 的构造)将不会改变。

正如您在上面看到的,boost::parser::search() 的一个方面与 std::ranges 算法的约定有刻意区别——它接受 C 风格字符串,并将它们视为正确的范围。

此外,boost::parser::search() 知道如何适应您的迭代器类型。您可以传递 C 风格字符串 "aaXYZq"(如上面的示例所示),或 "aaXYZq" | bp::as_utf32,或 "aaXYZq" | bp::as_utf8,甚至 "aaXYZq" | bp::as_utf16,并且它将返回一个子范围,其迭代器类型是您作为输入传递的类型,即使在内部迭代器类型可能不同(在 Unicode 解析中,UTF-8 -> UTF-32 转码迭代器,与上面所有 | bp::as_utfN 示例一样)。只要您传递一个值类型为 charchar8_tchar32_t 或通过 as_utfN 适配器组合适配的范围进行解析,这种适应就会正确运行。

boost::parser::search() 有多个重载。您可以传递一个范围或一个迭代器/哨兵对,也可以选择传递一个跳过解析器。这意味着有四种重载。此外,所有四个重载都在末尾接受一个可选的 boost::parser::trace 参数。这对于调查为什么您在输入中找不到预期的内容非常有用。

boost::parser::search_all

boost::parser::search_all 创建 boost::parser::search_all_viewsboost::parser::search_all_view 是一个 std::views 风格的视图。它生成一个子范围范围。它生成的每个子范围是在解析范围中给定解析器的下一个匹配项。给定解析器在解析范围中的下一个匹配项。

namespace bp = boost::parser;
auto r = "XYZaaXYZbaabaXYZXYZ" | bp::search_all(bp::lit("XYZ"));
int count = 0;
// Prints XYZ XYZ XYZ XYZ.
for (auto subrange : r) {
    std::cout << std::string_view(subrange.begin(), subrange.end() - subrange.begin()) << " ";
    ++count;
}
std::cout << "\n";
assert(count == 4);

上面关于 boost::parser::search() 的子节中指出的所有详细信息都适用于 boost::parser::search_all:其解析器不生成属性;它接受 C 风格字符串作为范围;并且它知道如何从内部使用的迭代器类型转换回给定的迭代器类型,在典型情况下。

boost::parser::search_all 可以使用,并且 boost::parser::search_all_view 可以使用不使用跳过解析器来构造,并且您始终可以在其任何重载的末尾传递 boost::parser::trace

boost::parser::split

boost::parser::split 创建 boost::parser::split_viewsboost::parser::split_view 是一个 std::views 风格的视图。它生成一个范围,该范围由解析范围被给定解析器匹配项分割成的子范围组成。您可以将 boost::parser::split_view 视为 boost::parser::search_all_view 的补集,因为 boost::parser::split_view 生成 boost::parser::search_all_view 生成的子范围之间的子范围。 boost::parser::split_view 具有与 std::views::split_view 非常相似的语义。就像 std::views::split_view 一样,boost::parser::split_view 将在解析范围的开头/结尾和相邻匹配项之间,或相邻匹配项之间生成空范围。

namespace bp = boost::parser;
auto r = "XYZaaXYZbaabaXYZXYZ" | bp::split(bp::lit("XYZ"));
int count = 0;
// Prints '' 'aa' 'baaba' '' ''.
for (auto subrange : r) {
    std::cout << "'" << std::string_view(subrange.begin(), subrange.end() - subrange.begin()) << "' ";
    ++count;
}
std::cout << "\n";
assert(count == 5);

上面关于 boost::parser::search() 的子节中指出的所有详细信息都适用于 boost::parser::split:其解析器不生成属性;它接受 C 风格字符串作为范围;并且它知道如何从内部使用的迭代器类型转换回给定的迭代器类型,在典型情况下。

boost::parser::split 可以使用,并且 boost::parser::split_view 可以使用不使用跳过解析器来构造,并且您始终可以在其任何重载的末尾传递 boost::parser::trace

boost::parser::replace
[Important] 重要提示

boost::parser::replaceboost::parser::replace_view 在 MSVC 中使用 C++17 模式时不可用。

boost::parser::replace 创建 boost::parser::replace_viewsboost::parser::replace_view 是一个 std::views 风格的视图。它从解析范围 r 和给定的替换范围 replacement 生成一个子范围范围。在解析范围中找到给定解析器 parser 的匹配项时,replacement 就是生成的子范围。 r 中不匹配 parser 的每个子范围也作为子范围生成。子范围按它们在 r 中出现的顺序生成。与 boost::parser::split_view 不同,boost::parser::replace_view 不生成空子范围,除非 replacement 为空。

namespace bp = boost::parser;
auto card_number = bp::int_ >> bp::repeat(3)['-' >> bp::int_];
auto rng = "My credit card number is 1234-5678-9012-3456." | bp::replace(card_number, "XXXX-XXXX-XXXX-XXXX");
int count = 0;
// Prints My credit card number is XXXX-XXXX-XXXX-XXXX.
for (auto subrange : rng) {
    std::cout << std::string_view(subrange.begin(), subrange.end() - subrange.begin());
    ++count;
}
std::cout << "\n";
assert(count == 3);

如果 rreplacement 范围的迭代器类型 IrIreplacement 相同(如上面的示例所示),则子范围的迭代器类型是 Ir。如果它们不同,则使用实现定义的类型作为迭代器。该类型在道德上等同于 std::variant<Ir, Ireplacement>。只要 IrIreplacement 兼容,这就可以工作。为了兼容,它们必须具有共同的引用、值和右值引用类型,如 std::common_type_t 所确定的。这种方案的一个优点是,boost::parser::replace_view 所表示的子范围范围很容易重新连接成一个单一范围。

namespace bp = boost::parser;
auto card_number = bp::int_ >> bp::repeat(3)['-' >> bp::int_];
auto rng = "My credit card number is 1234-5678-9012-3456." | bp::replace(card_number, "XXXX-XXXX-XXXX-XXXX") | std::views::join;
std::string replace_result;
for (auto ch : rng) {
    replace_result.push_back(ch);
}
assert(replace_result == "My credit card number is XXXX-XXXX-XXXX-XXXX.");

请注意,我们 不能 这样写:std::string replace_result(r.begin(), r.end())。这是格式错误的,因为 std::string 的范围构造函数接受两个相同类型的迭代器,但 decltype(rng.end()) 是一个与 decltype(rng.begin()) 不同的哨兵类型。

尽管范围 rreplacement 都可以是 C 风格字符串,但 boost::parser::replace_view 在执行任何操作之前必须知道 replacement 的结束。这是因为生成的子范围都是通用范围,所以如果 replacement 不是,就必须从它形成一个通用范围。如果您期望将很长的 C 风格字符串传递给 boost::parser::replace 并且不打算在范围使用之前查看其末尾,那么不要这样做。

ReplacementV 的约束几乎与 V 完全相同。 V 必须模型 parsable_rangestd::ranges::viewable_rangeReplacementV 也是如此,只是它可以是 std::ranges::input_range,而 V 必须是 std::ranges::forward_range

您可能想知道,当您为 r 传递 UTF-N 范围,为 replacement 传递 UTF-M 范围时会发生什么。在这种情况下,boost::parser::replace 范围适配器会默默地将 replacement 从 UTF-M 转码为 UTF-N。这不需要内存分配;boost::parser::replace 只是简单地将 | boost::parser::as_utfN 附加到 replacement 上。然而,由于 Boost.Parser 将 char 范围视为未知编码,boost::parser::replace 不会从 char 范围转码。因此,这样的调用将不起作用

char const str[] = "some text";
char const replacement_str[] = "some text";
using namespace bp = boost::parser;
auto r = empty_str | bp::replace(parser, replacement_str | bp::as_utf8); // Error: ill-formed!  Can't mix plain-char inputs and UTF replacements.

即使 char 和 UTF-8 大小相同,这也不会起作用。如果 rreplacement 都是 char 的范围,那么一切当然都会起作用。只是混合 char 和 UTF 编码的范围不起作用。

上面关于 boost::parser::search() 小节中提到的所有细节都适用于 boost::parser::replace:它的解析器不产生属性;它接受 C 风格字符串作为 rreplacement 参数,就好像它们是范围一样;并且它知道如何从内部使用的迭代器类型返回到给定的迭代器类型,在典型情况下。

boost::parser::replace 可以带或不带跳过解析器来调用,并且 boost::parser::replace_view 也可以带或不带跳过解析器来构造,并且您总可以在它们的任何重载的末尾传递 boost::parser::trace

boost::parser::transform_replace
[Important] 重要提示

boost::parser::transform_replaceboost::parser::transform_replace_view 在 C++17 模式下的 MSVC 上不可用。

[Important] 重要提示

boost::parser::transform_replaceboost::parser::transform_replace_view 在 C++20 模式下的 GCC 12 之前不可用。

boost::parser::transform_replace 创建 boost::parser::transform_replace_viewsboost::parser::transform_replace_view 是一个 std::views 风格的视图。它会从解析范围 r 和给定的可调用对象 f 中产生子范围的范围。 wherever in the parsed range a match to the given parser parser is found, let parser's attribute be attr; f(std::move(attr)) is the subrange produced. 在解析范围中任何与给定解析器 parser 匹配的地方,令 parser 的属性为 attrf(std::move(attr)) 是产生的子范围。与 parser 匹配的 r 的每个子范围也会被作为子范围产生。子范围按它们在 r 中出现的顺序产生。与 boost::parser::split_view 不同,boost::parser::transform_replace_view 不会产生空子范围,除非 f(std::move(attr)) 为空。下面是一个例子。

auto string_sum = [](std::vector<int> const & ints) {
    return std::to_string(std::accumulate(ints.begin(), ints.end(), 0));
};

auto rng = "There are groups of [1, 2, 3, 4, 5] in the set." |
           bp::transform_replace('[' >> bp::int_ % ',' >> ']', bp::ws, string_sum);
int count = 0;
// Prints "There are groups of 15 in the set".
for (auto subrange : rng) {
    for (auto ch : subrange) {
        std::cout << ch;
    }
    ++count;
}
std::cout << "\n";
assert(count == 3);

decltype(f(std::move(attr))) 的类型为 ReplacementReplacement 必须是范围,并且必须与 r 兼容。有关详细信息,请参阅上面关于 boost::parser::replace_view 的迭代器兼容性要求的描述。

boost::parser::replace 一样,boost::parser::transform_replace 可以通过将其通过管道传输到 std::views::join 来从子范围视图展平为元素视图。有关示例,请参阅上面关于 boost::parser::replace 的章节。

boost::parser::replaceboost::parser::replace_view 一样,boost::parser::transform_replaceboost::parser::transform_replace_view 会在适用时对结果进行静默转码以得到适当的 UTF。如果 rf(std::move(attr)) 都是 char 的范围,或者都是相同的 UTF,则不进行转码。如果 rf(std::move(attr)) 中一个是 char 的范围,另一个是某种 UTF,则程序格式错误。

boost::parser::transform_replace_view 会将每个属性移动到 ff 可以根据需要移动或复制参数。如果 f 返回左值引用,则会获取该引用的地址并存储在 boost::parser::transform_replace_view 中。否则,f 返回的值将被移动到 boost::parser::transform_replace_view 中。无论哪种情况,boost::parser::transform_replace_view 的值类型始终是子范围。

boost::parser::transform_replace 可以带或不带跳过解析器来调用,并且 boost::parser::transform_replace_view 也可以带或不带跳过解析器来构造,并且您总可以在它们的任何重载的末尾传递 boost::parser::trace

Boost.Parser 从一开始就被设计成 Unicode 友好。在 Boost.Parser 文档中,有大量关于“Unicode 代码路径”和“非 Unicode 代码路径”的引用。虽然 Unicode 和非 Unicode 解析确实存在两条代码路径,但这两条代码路径的代码差异不大,因为它们是以通用方式编写的。唯一的区别是 Unicode 代码路径将输入解析为代码点范围,而非 Unicode 路径则不。实际上,这意味着在 Unicode 代码路径中,当您为某个输入范围 r 和某个解析器 p 调用 parse(r, p) 时,其解析过程就好像您调用 parse(r | boost::parser::as_utf32, p) 一样。(当然,r 是一个合适的范围,还是迭代器/哨兵对,这并不重要;这些都与 boost::parser::as_utf32 很好地配合。)

Boost.Parser 的解析器中,“字符”的匹配被假定为代码点匹配。在 Unicode 路径中,输入中的每个代码点都与每个 char_ 解析器进行匹配。在非 Unicode 路径中,编码是未知的,因此输入的每个元素都被视为输入编码中的一个完整“字符”,类似于代码点。因此,从这一点开始,我将只把输入的单个元素称为代码点。

所以,假设我们写了这样的解析器

constexpr auto char8_parser = boost::parser::char_('\xcc');

对于任何 char_ 解析器,如果它应该匹配一个或多个值,那么要匹配的值的类型将被保留。因此,char8_parser 包含一个 char,它将用于匹配。如果我们写了

constexpr auto char32_parser = boost::parser::char_(U'\xcc');

char32_parser 将包含一个 char32_t,它将用于匹配。

因此,在解析过程中的任何时候,如果 char8_parser 用于匹配输入中的代码点 next_cp,我们就会看到 next_cp == '\xcc' 的道德等价物,而如果 char32_parser 用于匹配 next_cp,我们就会看到 next_cp == U'\xcc' 的等价物。由此得出的结论是,您可以编写匹配特定值的 char_ 解析器,而无需担心输入是否是 Unicode,因为底层发生的是两个整数值的简单比较。

[Note] 注意

Boost.Parser 实际上使用 std::common_type 将任何两个值提升到公共类型,然后再进行比较。这几乎总是有效的,因为输入和传递给 char_ 的任何参数都必须是字符类型。

由于匹配始终在代码点级别进行(请记住,在非 Unicode 路径中,“代码点”被假定为一个 char),因此在 Unicode 和非 Unicode 代码路径中尝试匹配 UTF-8 输入时,您会得到不同的结果。

namespace bp = boost::parser;

{
    std::string str = (char const *)u8"\xcc\x80"; // encodes the code point U+0300
    auto first = str.begin();

    // Since we've done nothing to indicate that we want to do Unicode
    // parsing, and we've passed a range of char to parse(), this will do
    // non-Unicode parsing.
    std::string chars;
    assert(bp::parse(first, str.end(), *bp::char_('\xcc'), chars));

    // Finds one match of the *char* 0xcc, because the value in the parser
    // (0xcc) was matched against the two code points in the input (0xcc and
    // 0x80), and the first one was a match.
    assert(chars == "\xcc");
}
{
    std::u8string str = u8"\xcc\x80"; // encodes the code point U+0300
    auto first = str.begin();

    // Since the input is a range of char8_t, this will do Unicode
    // parsing.  The same thing would have happened if we passed
    // str | boost::parser::as_utf32 or even str | boost::parser::as_utf8.
    std::string chars;
    assert(bp::parse(first, str.end(), *bp::char_('\xcc'), chars));

    // Finds zero matches of the *code point* 0xcc, because the value in
    // the parser (0xcc) was matched against the single code point in the
    // input, 0x0300.
    assert(chars == "");
}
Implicit transcoding

此外,预计大多数程序将使用 UTF-8 来编码 Unicode 字符串。Boost.Parser 是以这种情况为出发点编写的。这意味着,如果您解析 32 位代码点(在 Unicode 路径中始终如此),并且想将结果捕获到 charchar8_t 值的容器 C 中,Boost.Parser 将会静默地将 UTF-32 转码为 UTF-8 并将属性写入 C。这意味着 std::stringstd::u8string 等可以用作 *char_ 的属性输出参数,结果将是 UTF-8。

[Note] 注意

UTF-16 字符串作为属性不直接支持。如果您想将 UTF-16 字符串用作属性,您可能需要通过在语义动作中将 UTF-8 或 UTF-32 属性转码为 UTF-16 来实现。您可以通过使用 boost::parser::as_utf16 来实现。

UTF-8 字符串的表示在 Boost.Parser 中几乎无处不在。例如,尽管 symbols 的整个接口都使用 std::stringstd::string_view,但内部使用的是 UTF-32 比较。

Explicit transcoding

我上面提到,使用 boost::parser::utf*_view 作为要解析的范围,您就选择了 Unicode 解析。关于这些视图以及如何最好地使用它们,还有更多信息。

如果您想进行 Unicode 解析,您总会在解析的每一步都比较代码点。因此,如有必要,您将隐式地将任何解析输入转换为 UTF-32。这正是所有解析 API 函数在内部执行的操作。

然而,有时您会遇到输入是 UTF-8 编码的 char 序列,而您想进行 Unicode 感知的解析。如前所述,Boost.Parser 对 char 输入有一个特殊处理,它不会假定 char 序列是 UTF-8。如果您想让解析 API 仍然对它们执行 Unicode 处理,您可以使用 as_utf32 范围适配器。(请注意,您可以使用任何 as_utf* 适配器,语义不会与下面的语义不同。)

namespace bp = boost::parser;

auto const p = '"' >> *(bp::char_ - '"' - 0xb6) >> '"';
char const * str = "\"two wörds\""; // ö is two code units, 0xc3 0xb6

auto result_1 = bp::parse(str, p);                // Treat each char as a code point (typically ASCII).
assert(!result_1);
auto result_2 = bp::parse(str | bp::as_utf32, p); // Unicode-aware parsing on code points.
assert(result_2);

第一次调用 parse() 时,将每个 char 视为一个代码点,并且由于 "ö" 是代码单元 0xc3 0xb6 的对,因此解析将第二个代码单元与上面解析器的 - 0xb6 部分匹配,导致解析失败。发生这种情况是因为 str 中的每个代码单元/char 都被视为独立的 कोड 点。

第二次调用 parse() 成功,因为当解析到达 'ö' 的代码点时,它是 0xf6 (U+00F6),这与解析器的 - 0xb6 部分不匹配。

为了完整起见,还提供了其他适配器 as_utf8as_utf16,如果您想使用它们的话。它们都可以转码任何字符类型的序列。

[Important] 重要提示

as_utfN 适配器是可选的,因此它们不包含在 parser.hpp 中。要访问它们,请 #include <boost/parser/transcode_view.hpp>

(Lack of) normalization

Boost.Parser 不处理的一件事是规范化;Boost.Parser 完全不考虑规范化。由于所有解析器都使用代码点的相等性比较进行匹配,因此您应该确保您的解析范围和您的解析器都使用相同的规范化形式。

在大多数解析情况下,能够生成表示解析结果的属性,或者能够解析到这样的属性,就已经足够了。有时则不然。如果您需要解析非常大的文本块,生成的属性可能太大而无法放入内存。在其他情况下,您可能希望有时生成属性,有时不生成。 callback_rules 就是为了这些用途而存在的。 callback_rule 就像一个规则一样,但它允许在以调用 callback_parse() 而不是 parse() 来启动解析时,通过回调将规则的属性返回给调用者。在调用 parse() 的过程中,callback_rule 与普通 rule 完全相同。

对于没有属性的规则,回调函数的签名是 void (tag),其中 tag 是声明规则时使用的标签类型。对于具有属性 attr 的规则,签名是 void (tag, attr)。例如,对于这个规则

boost::parser::callback_rule<struct foo_tag> foo = "foo";

这将是一个合适的回调函数

void foo_callback(foo_tag)
{
    std::cout << "Parsed a 'foo'!\n";
}

对于这个规则

boost::parser::callback_rule<struct bar_tag, std::string> bar = "bar";

这将是一个合适的回调函数

void bar_callback(bar_tag, std::string const & s)
{
    std::cout << "Parsed a 'bar' containing " << s << "!\n";
}
[Important] 重要提示

bar_callback() 的情况下,我们不需要除了插入流之外对 s 做任何事情,所以我们将其作为 const 左值引用。Boost.Parser 会将所有属性移动到回调中,所以签名也可以是 void bar_callback(bar_tag, std::string s)void bar_callback(bar_tag, std::string && s)

您可以通过调用 callback_parse() 而不是 parse() 来选择回调解析。如果您在 parse() 中使用 callback_rules,它们就只是普通的 rules。这使您可以选择是进行“正常”的生成属性/分配属性的解析(使用 parse()),还是进行回调解析(使用 callback_parse()),而无需重写大量解析代码(如果有的话)。

所有 rules 都不是 callback_rules 的唯一原因是,您可能希望在解析中使用一些 rules 使用回调,而另一些则不使用。例如,如果您想通过回调报告 callback_rule r1 的属性,那么 r1 的实现可能会使用某个规则 r2 来生成其部分或全部属性。

有关回调解析的扩展示例,请参阅 Parsing JSON With Callbacks

Error handling

Boost.Parser 内置了良好的错误报告功能。考虑一下当我们在期望点(使用 operator> 创建)解析失败时会发生什么。如果我将来自 Parsing JSON With Callbacks 示例的解析器输入一个名为 sample.json 的文件,其中包含以下输入(请注意未匹配的 '['

{
    "key": "value",
    "foo": [, "bar": []
}

这是打印到终端的错误消息

sample.json:3:12: error: Expected ']' here:
    "foo": [, "bar": []
            ^

此消息的格式与 Clang 和 GCC 生成的诊断信息类似。它会引用失败发生在哪一行,甚至在该行的确切失败位置下方标记一个插入符号。此错误消息适合许多种类的最终用户,并且可以与支持 Clang 和/或 GCC 诊断的任何工具很好地互操作。

Boost.Parser 的大多数错误处理程序都以这种方式格式化其诊断信息,但您不受此约束。您可以创建一个错误处理程序类型,使其执行您想要的任何操作,只要它符合错误处理程序接口。

Boost.Parser 的错误处理程序是

  • default_error_handler: 生成上述格式的诊断信息,并将其打印到 std::cerrdefault_error_handler 没有关联的文件名,错误和诊断信息都打印到 std::cerr。此处理程序符合 constexpr
  • stream_error_handler: 生成格式化的诊断信息。可以使用一个或两个流。如果使用两个流,错误会转到其中一个流,警告会转到另一个流。可以将文件名与解析关联起来;如果关联了文件名,则该文件名将出现在所有诊断信息中。
  • callback_error_handler: 生成格式化的诊断信息。调用回调函数处理诊断消息,而不是将诊断信息流式传输出去。可以将文件名与解析关联起来;如果关联了文件名,则该文件名将出现在所有诊断信息中。此处理程序对于在内存中记录诊断信息很有用。
  • rethrow_error_handler: 除了重新抛出它被要求处理的任何异常之外,不做任何事情。它的 diagnose() 成员函数是空操作。
  • vs_output_error_handler: 将所有错误和警告定向到 Visual Studio 内部的调试输出面板。仅在 Windows 上可用。在 Visual Studio 外部执行时可能没有用处。

您可以使用 with_error_handler()(请参阅 The parse() API)将错误处理程序设置为以上任何一种,或您自己的处理程序。如果您不设置,则将使用 default_error_handler

How diagnostics are generated

Boost.Parser 仅在期望点失败(例如 a > b,其中您已成功解析 a,但随后无法成功解析 b)和输入意外结束时生成本页中的错误消息。这可能显得有限。实际上,这是我们能做到的最好的了。

为了在期望点之外进行错误处理,我们必须知道没有进一步的处理可能发生。这是因为 Boost.Parser 有 P1 | P2 | ... | Pn 解析器(“or_parser”)。如果其中任何一个解析器 Pi 匹配失败,则不允许它导致解析失败——下一个解析器(Pi+1)可能匹配。如果我们到达 or_parser 的备选项末尾并且 Pn 失败,我们仍然不能使顶层解析失败,因为这个 or_parser 可能位于父级 or_parser 的子解析器中。唯一例外是:我们已经完成了顶层解析;顶层解析不是前缀解析;并且输入范围仍有剩余部分。在这种情况下,有一个隐含的期望,即解析结束和输入结束是同一位置,而这个隐含的期望刚刚被违反。

请注意,当遇到输入结束时,我们不能使顶层解析失败。出于与前面相同的确切原因,我们无法做到。对于任何解析器 P,到达输入结束对 P 来说是一个失败,但并不一定是整个解析的失败。

好的,那么我们还能进行哪些其他类型的错误报告呢?也许我们可以记录在解析过程中达到的最远点,并在顶层解析失败时报告它。这在不知道在哪个解析器处于活动状态时到达该点的情况下几乎没有帮助。这需要某种重复的内存分配,因为在 Boost.Parser 中,解析器的进度点仅存储在堆栈上——当我们使顶层解析失败时,所有那些远距离的堆栈帧早已消失。并非最佳。

更糟糕的是,知道解析的进度和活动解析器也很有用。考虑一下这个。

namespace bp = boost::parser;
auto a_b = bp::char_('a') >> bp::char_('b');
auto c_b = bp::char_('c') >> bp::char_('b');
auto result = bp::parse("acb", a_b | c_b);

如果我们报告了最远的解析器及其位置,它将是 a_b 解析器,在输入位置 "bc"。这真的有启发性吗?错误是在输入的开头放 'a',还是在中间放 'c'?如果您将用户指向 a_b 作为失败的解析器,而从不提及 c_b,那么您可能只是将他们引向了错误的方向。

所有错误消息都必须来自失败的期望点(或意外的输入结束)。考虑解析 JSON。如果您用 '[' 打开一个列表,您就知道您正在解析一个列表,如果列表格式不正确,您会收到一条错误消息。如果您用 '{' 打开一个对象,情况也是如此——在缺少匹配的 '}' 时,您可以告诉用户“这不是一个对象”,这提供了有用的反馈。部分解析的数字等也是如此。如果 JSON 解析器不建立像匹配的花括号和方括号这样的期望,Boost.Parser 如何知道缺少 '}' 确实是一个问题,并且没有后续解析器可以在没有 '}' 的情况下匹配输入?

[Important] 重要提示

最重要的是,您应该尽可能多地使用 operator> 将期望点构建到您的解析器中。

Using error handlers in semantic actions

您可以通过调用 _error_handler(ctx)(请参阅 The Parse Context)在任何语义动作中访问错误处理程序。任何错误处理程序都必须具有以下成员函数

template<typename Context, typename Iter>
void diagnose(
    diagnostic_kind kind,
    std::string_view message,
    Context const & context,
    Iter it) const;

template<typename Context>
void diagnose(
    diagnostic_kind kind,
    std::string_view message,
    Context const & context) const;

如果您调用第二个,即不带迭代器参数的那个,它将调用第一个,并将 _where(context).begin() 作为迭代器参数。不带迭代器的那个是您最常使用的。带有显式迭代器参数的那个在消息彼此相关,并与多个位置相关的情况下可能很有用。例如,如果您正在解析 XML,您可能想报告一个结束标签与其对应的开始标签不匹配,通过显示找到开始标签的行。当然,这可能不在 _where(ctx).begin() 附近。 (下面将描述 _globals()。)

[](auto & ctx) {
    // Assume we have a std::vector of open tags, and another
    // std::vector of iterators to where the open tags were parsed, in our
    // globals.
    if (_attr(ctx) != _globals(ctx).open_tags.back()) {
        std::string open_tag_msg =
            "Previous open-tag \"" + _globals(ctx).open_tags.back() + "\" here:";
        _error_handler(ctx).diagnose(
            boost::parser::diagnostic_kind::error,
            open_tag_msg,
            ctx,
            _globals(ctx).open_tags_position.back());
        std::string close_tag_msg =
            "does not match close-tag \"" + _attr(ctx) + "\" here:";
        _error_handler(ctx).diagnose(
            boost::parser::diagnostic_kind::error,
            close_tag_msg,
            ctx);

        // Explicitly fail the parse.  Diagnostics do not affect parse success.
        _pass(ctx) = false;
    }
}
_report_error() and _report_warning()

还有一些方便函数可以使上述代码不那么冗长,即 _report_error()_report_warning()

[](auto & ctx) {
    // Assume we have a std::vector of open tags, and another
    // std::vector of iterators to where the open tags were parsed, in our
    // globals.
    if (_attr(ctx) != _globals(ctx).open_tags.back()) {
        std::string open_tag_msg =
            "Previous open-tag \"" + _globals(ctx).open_tags.back() + "\" here:";
        _report_error(ctx, open_tag_msg, _globals(ctx).open_tag_positions.back());
        std::string close_tag_msg =
            "does not match close-tag \"" + _attr(ctx) + "\" here:";
        _report_error(ctx, close_tag_msg);

        // Explicitly fail the parse.  Diagnostics do not affect parse success.
        _pass(ctx) = false;
    }
}

您应该几乎一直使用这些不那么冗长的函数。您想直接使用 _error_handler() 的唯一情况是,当您使用自定义错误处理程序并且除了 diagnose() 之外还需要访问其接口的某些部分时。

尽管有支持使用上述函数报告警告的功能,但 Boost.Parser 提供的任何错误处理程序都不会报告警告。警告严格用于用户代码。

有关错误处理和诊断 API 的其余部分,请参阅 error_handling_fwd.hpperror_handling.hpp 的头文件引用页面。

Creating your own error handler

创建自己的错误处理程序非常简单;您只需要实现三个成员函数。假设您想要一个将诊断信息写入文件的错误处理程序。您可以这样实现:

struct logging_error_handler
{
    logging_error_handler() {}
    logging_error_handler(std::string_view filename) :
        filename_(filename), ofs_(filename_)
    {
        if (!ofs_)
            throw std::runtime_error("Could not open file.");
    }

    // This is the function called by Boost.Parser after a parser fails the
    // parse at an expectation point and throws a parse_error.  It is expected
    // to create a diagnostic message, and put it where it needs to go.  In
    // this case, we're writing it to a log file.  This function returns a
    // bp::error_handler_result, which is an enum with two enumerators -- fail
    // and rethrow.  Returning fail fails the top-level parse; returning
    // rethrow just re-throws the parse_error exception that got us here in
    // the first place.
    template<typename Iter, typename Sentinel>
    bp::error_handler_result
    operator()(Iter first, Sentinel last, bp::parse_error<Iter> const & e) const
    {
        bp::write_formatted_expectation_failure_error_message(
            ofs_, filename_, first, last, e);
        return bp::error_handler_result::fail;
    }

    // This function is for users to call within a semantic action to produce
    // a diagnostic.
    template<typename Context, typename Iter>
    void diagnose(
        bp::diagnostic_kind kind,
        std::string_view message,
        Context const & context,
        Iter it) const
    {
        bp::write_formatted_message(
            ofs_,
            filename_,
            bp::_begin(context),
            it,
            bp::_end(context),
            message);
    }

    // This is just like the other overload of diagnose(), except that it
    // determines the Iter parameter for the other overload by calling
    // _where(ctx).
    template<typename Context>
    void diagnose(
        bp::diagnostic_kind kind,
        std::string_view message,
        Context const & context) const
    {
        diagnose(kind, message, context, bp::_where(context).begin());
    }

    std::string filename_;
    mutable std::ofstream ofs_;
};

就这样。您只需要在调用运算符中执行错误处理程序的重要工作,然后实现它必须提供的 diagnose() 的两个重载,以便在语义动作中使用。这些的默认实现甚至可以作为自由函数 write_formatted_message() 提供,所以您可以看到,您可以直接调用它。下面是如何使用它。

int main()
{
    std::cout << "Enter a list of integers, separated by commas. ";
    std::string input;
    std::getline(std::cin, input);

    constexpr auto parser = bp::int_ >> *(',' > bp::int_);
    logging_error_handler error_handler("parse.log");
    auto const result = bp::parse(input, bp::with_error_handler(parser, error_handler));

    if (result) {
        std::cout << "It looks like you entered:\n";
        for (int x : *result) {
            std::cout << x << "\n";
        }
    }
}

我们只需定义一个 logging_error_handler,并通过引用将其传递给 with_error_handler(),后者使用错误处理程序装饰顶层解析器。我们不能bp::with_error_handler(parser, logging_error_handler("parse.log")),因为 with_error_handler() 不接受右值。这是因为错误处理程序最终会进入解析上下文。解析上下文只存储指针和迭代器,使其复制成本低廉。

如果我们运行示例并输入 "1,",这会出现在日志文件中

parse.log:1:2: error: Expected int_ here (end of input):
1,
  ^
Fixing ill-formed code

有时,在编写解析器时,您会犯一个简单的错误,该错误会产生可怕的诊断,因为从您刚刚编写的行到使用点(通常是调用 parse())之间的模板实例化次数很多。所谓“有时”,我指的是“几乎总是,并且很多很多次”。Boost.Parser 为这种情况提供了一个变通方法。这个变通方法是在尽可能多的情况下使格式错误的(ill-formed)代码格式正确(well-formed),然后改为进行运行时断言。

通常,C++ 程序员会尽量尽早捕获错误。这通常意味着使尽可能多的错误代码格式不正确。反直觉的是,这在解析器组合器情况下效果不佳。有关 Boost.Parser 中这两种调试场景有多么显著不同的示例,请参阅 none is weird 部分的 Rationale 中非常长的讨论。

如果您在道德上反对这种方法,或者只是讨厌乐趣,那么好消息是:您可以通过定义 BOOST_PARSER_NO_RUNTIME_ASSERTIONS 来完全关闭此技术的使用。

Runtime debugging

调试解析器是困难的。任何高于一定复杂度的解析器,仅通过查看解析器代码都几乎不可能调试。在调试器中单步执行解析更糟。为了给调试解析器提供合理的可能性,Boost.Parser 有一个跟踪模式,您可以通过向 parse()callback_parse() 传递额外的参数来打开它。

boost::parser::parse(input, parser, boost::parser::trace::on);

每个 parse()callback_parse() 的重载都接受此最终参数,该参数默认为 boost::parser::trace::off

如果我们跟踪一个相当复杂的解析器,我们会看到大量的输出。输入中的每个代码点都必须逐一考虑,以查看某个规则是否匹配。例如,让我们跟踪使用 Parsing JSON 中的 JSON 解析器进行的解析。输入是 "null"null 是 JavaScript 值可以具有的类型之一;JSON 解析器示例中的顶层解析器是

auto const value_p_def =
    number | bp::bool_ | null | string | array_p | object_p;

所以,JSON 值可以是数字,或者布尔值,null,等等。在解析过程中,每个备选项将依次尝试,直到一个被匹配。我选择了 null,因为它相对接近 value_p_def 备选项解析器的开头。即使如此,输出还是相当大的。让我们逐步分解一下

[begin value; input="null"]

每个解析器都显示为 [begin foo; ...],然后是解析操作本身,最后是 [end foo; ...]。规则的名称用作跟踪的 beginend 部分中的名称。非规则的名称类似于编写解析器时的样子。大多数行都会引用输入中的下一个几个代码点,就像这里一样(input="null")。

[begin number | bool_ | null | string | ...; input="null"]

这显示了规则 value 内部的解析器的开头——实际上执行所有工作的解析器。在示例代码中,此解析器称为 value_p_def。由于它不是一个规则,所以我们没有名称,因此我们显示它在子解析器方面的实现。由于它有点长,我们不打印全部。这就是为什么那里有省略号。

[begin number; input="null"]
  [begin raw[lexeme[ >> ...]][<<action>>]; input="null"]

现在我们开始看到实际工作了。number 是一个相当复杂的解析器,它不匹配 "null",因此在跟踪其尝试匹配 "null" 的过程中,需要了解很多内容。需要注意的一点是,由于我们无法为动作打印名称,因此我们只打印 "<<action>>"。当遇到无法打印的属性(因为它没有流插入运算符)时,也会发生类似的情况。在这种情况下,将打印 "<<unprintable-value>>"

    [begin raw[lexeme[ >> ...]]; input="null"]
      [begin lexeme[-char_('-') >> char_('1', '9') >> ... | ... >> ...]; input="null"]
        [begin -char_('-') >> char_('1', '9') >> *digit | char_('0') >> -(char_('.') >> ...) >> -( >> ...); input="null"]
          [begin -char_('-'); input="null"]
            [begin char_('-'); input="null"]
              no match
            [end char_('-'); input="null"]
            matched ""
            attribute: <<empty>>
          [end -char_('-'); input="null"]
          [begin char_('1', '9') >> *digit | char_('0'); input="null"]
            [begin char_('1', '9') >> *digit; input="null"]
              [begin char_('1', '9'); input="null"]
                no match
              [end char_('1', '9'); input="null"]
              no match
            [end char_('1', '9') >> *digit; input="null"]
            [begin char_('0'); input="null"]
              no match
            [end char_('0'); input="null"]
            no match
          [end char_('1', '9') >> *digit | char_('0'); input="null"]
          no match
        [end -char_('-') >> char_('1', '9') >> *digit | char_('0') >> -(char_('.') >> ...) >> -( >> ...); input="null"]
        no match
      [end lexeme[-char_('-') >> char_('1', '9') >> ... | ... >> ...]; input="null"]
      no match
    [end raw[lexeme[ >> ...]]; input="null"]
    no match
  [end raw[lexeme[ >> ...]][<<action>>]; input="null"]
  no match
[end number; input="null"]
[begin bool_; input="null"]
  no match
[end bool_; input="null"]

numberboost::parser::bool_ 没有匹配,但 null 会匹配

[begin null; input="null"]
  [begin "null" >> attr(null); input="null"]
    [begin "null"; input="null"]
      [begin string("null"); input="null"]
        matched "null"
        attribute:
      [end string("null"); input=""]
      matched "null"
      attribute: null

最后,这个解析器实际上匹配了,并且匹配生成了属性 null,这是一个 json::value 类型的特殊值。由于我们匹配的是字符串字面量 "null",因此之前直到 attr(null) 解析器才出现属性。

        [end "null"; input=""]
        [begin attr(null); input=""]
          matched ""
          attribute: null
        [end attr(null); input=""]
        matched "null"
        attribute: null
      [end "null" >> attr(null); input=""]
      matched "null"
      attribute: null
    [end null; input=""]
    matched "null"
    attribute: null
  [end number | bool_ | null | string | ...; input=""]
  matched "null"
  attribute: null
[end value; input=""]
--------------------
parse succeeded
--------------------

在解析结束时,跟踪代码会打印出顶层解析是否成功。

查看 Boost.Parser 跟踪输出时需要注意的一些事项

  • 有一些解析器您可能不知道,因为它们没有直接文档记录。例如,p[a] 形成一个 action_parser,其中包含解析器 p 和语义动作 a。这基本上是一个实现细节,但不幸的是,跟踪输出并没有向您隐藏这一点。
  • 对于解析器 p,跟踪名称可能与 p 的实际结构有意不同。例如,在上面的跟踪中,您会看到一个名为 "null" 的解析器。此解析器实际上是 boost::parser::omit[boost::parser::string("null")],但您通常只写 "null",所以这就是使用的名称。有两个特殊情况是这样的:这里描述的 omit[string] 的情况,以及另一个 omit[char_] 的情况。
  • 由于解析器名称的打印方式没有其他特殊情况,因此您可能会看到与您在代码中编写的不相关的解析器。在关于解析器和组合操作的部分中,您有时会看到一个解析器或组合操作根据等效的解析器进行描述。例如,if_(pred)[p] 被描述为“等同于 eps(pred) >> p”。在跟踪中,您将看不到 if_;您将看到 epsp
  • 传递给解析器的参数值在可能的情况下都会被打印出来。有时,解析参数本身不是值,而是可调用对象,它会产生该值。在这些情况下,您将看到解析参数的已解析值。

Boost.Parser 很少分配内存。例外情况包括

  • symbols 为其包含的符号/属性对分配内存。如果在解析过程中添加了符号,也必须在那时发生分配。 symbols 使用的数据结构也是一个前缀树(trie),它是一个基于节点的树。因此,如果您使用 symbols,很可能会发生大量分配。
  • 可以接受文件名的错误处理程序会在提供文件名时为其分配内存。
  • 如果通过将 boost::parser::trace::on 传递给顶层解析函数来打开跟踪,则会分配解析器名称。
  • 当遇到失败的期望(使用 operator>)时,失败的解析器的名称会被放入一个 std::string 中,这通常会导致一次分配。
  • string() 的属性是 std::string,使用它意味着分配。您可以显式使用另一种不进行分配的字符串类型作为属性来避免此分配。
  • repeat(p) 的所有形式(包括 operator*operator+operator%)的属性是 std::vector<ATTR(p)>,使用它意味着分配。您可以显式使用另一种不进行分配的序列容器作为属性来避免此分配。 boost::container::static_vector 或 C++26 的 std::inplace_vector 可能有助于此类替换。

除了在失败的期望情况下分配解析器名称之外,Boost.Parser 不会进行分配,除非您通过使用 symbols、使用特定的错误处理程序、打开跟踪或解析到会分配的属性来指示它。

Parse unicode from the start

如果您想解析 ASCII,使用 Unicode 解析 API 实际上不会花费您任何东西。您的输入将被解析,charchar,并与 Unicode 代码点(即 char32_t)进行比较。有一个注意事项是,如果输入是 UTF-8,则每个 char 可能有一个额外的分支。如果您的性能要求可以容忍这一点,那么从 Unicode 开始并坚持使用它将使您的生活轻松得多。

从一开始就支持 Unicode 和 UTF-8 输入将使您能够正确处理意外输入,例如非 ASCII 语言(大多数语言),而无需您付出额外的努力。

Write rules, and test them in isolation

将规则视为解析器中的工作单元。编写一个规则,测试其边界情况,然后使用它来构建更大的规则或解析器。这可以让你用更少的工作获得更好的覆盖率,因为逐一执行规则的所有代码路径可以使你的代码中的路径组合数量变得易于管理。

Prefer auto-generated attributes to semantic actions

有多种方法可以从解析器中获取属性。您可以

  • 使用解析器生成的任何属性;
  • parse() 提供属性输出参数,供解析器填充;
  • 使用一个或多个语义动作将属性从解析器分配给解析器外部的变量;
  • 使用回调解析通过回调调用提供属性。

所有这些在工作量上都相当相似,除了语义动作方法。对于语义动作方法,您需要有值来填充解析器,并在解析期间将其保持在范围内。

让解析器直接产生解析的属性作为解析结果,这更直接,并且可以产生更可重用的解析器。

这并不意味着您永远不应该使用语义动作。它们有时是必要的。但是,您应该默认使用其他非语义动作方法,并且只有在有充分理由时才使用语义动作。

If your parser takes end-user input, give rules names that you would want an end-user to see

Boost.Parser 生成的典型错误消息可能类似于“在此处期望 FOO”,其中 FOO 是某个规则或解析器。为您的规则命名,以便在这样的错误消息中读起来顺畅。例如,JSON 示例中有这些规则

bp::rule<class escape_seq, uint32_t> const escape_seq =
    "\\uXXXX hexadecimal escape sequence";
bp::rule<class escape_double_seq, uint32_t, double_escape_locals> const
    escape_double_seq = "\\uXXXX hexadecimal escape sequence";
bp::rule<class single_escaped_char, uint32_t> const single_escaped_char =
    "'\"', '\\', '/', 'b', 'f', 'n', 'r', or 't'";

需要注意的一些事项

- escape_seqescape_double_seq 具有相同的名称字符串。对于试图弄清楚为什么他们的输入未能解析的最终用户来说,解析规则生成哪种结果并不重要。他们只想知道如何修复他们的输入。对于任一规则,修复都是相同的:在该位置放置一个十六进制转义序列。

- single_escaped_char 的名称看起来很糟糕。然而,它实际上并没有被用作名称。在错误消息中,它工作得很好。错误将是“在此处期望 ', '', '/', 'b', 'f', 'n', 'r' 或 't'”,这相当有帮助。

Have a simple test that you can run to find ill-formed-code-as-asserts

这些错误大多数在解析器构造时被发现,因此甚至不需要实际的解析。例如,一个测试用例如下

TEST(my_parser_tests, my_rule_test) {
    my_rule r;
}

您可能永远不需要编写自己的低级解析器。您有 char_ 等基本类型,可以从中构建您需要的解析器。您不太可能需要比单个字符更低级别的操作。

然而。有些人痴迷于自己编写所有东西。我们称他们为 C++ 程序员。这一节是为他们准备的。但是,这一节并不是一个深入的教程。它是一个基本的方向,让您熟悉编写解析器所需的所有活动部件,然后您可以阅读 Boost.Parser 代码进行学习。

每个解析器都必须提供两个 call() 函数的重载。一个重载进行解析,产生一个属性(可能是特殊的无属性类型 detail::nope)。另一个进行解析,填充一个给定的属性。给定属性的类型是一个模板参数,因此它可以接受任何可以引用(reference)的类型。

让我们看一下 Boost.Parser 解析器 opt_parser。这是通过使用 operator- 产生的解析器。首先,这是其定义开头。

template<typename Parser>
struct opt_parser
{

其定义的结尾是

    Parser parser_;
};

正如您所见,opt_parser 的唯一数据成员是它适配的解析器 parser_。这是其生成属性的 call() 重载。

template<
    typename Iter,
    typename Sentinel,
    typename Context,
    typename SkipParser>
auto call(
    Iter & first,
    Sentinel last,
    Context const & context,
    SkipParser const & skip,
    detail::flags flags,
    bool & success) const
{
    using attr_t = decltype(parser_.call(
        first, last, context, skip, flags, success));
    detail::optional_of<attr_t> retval;
    call(first, last, context, skip, flags, success, retval);
    return retval;
}

首先,让我们看一下模板和函数参数。

  • Iter & first 是迭代器。它被作为输出参数传递。 call() 的职责是,当且仅当解析成功时,才推进 first
  • Sentinel last 是哨兵。如果在 call() 中解析尚未成功,并且 first == lasttrue,那么 call() 必须失败(通过将 bool & success 设置为 false)。
  • Context const & context 是解析上下文。它将是 detail::parse_context 的某个特例。上下文用于任何对子解析器 call() 的调用,并且在某些情况下,应该创建一个新的上下文,并将新上下文传递给子解析器;更多内容将在下面讨论。
  • SkipParser const & skip 是当前的跳过解析器。 skip 应该在解析开始时以及在任何两个子解析器使用之间使用。
  • detail::flags flags 是一个标志集合,指示当前解析状态的各种内容。 flags 关注的是是否生成属性;是否应用跳过解析器 skip;是否生成详细的跟踪(如顶层传递 boost::parser::trace::on 时);以及我们当前是否在实用函数 detail::apply_parser 中。
  • bool & success 是最终的函数参数。如果解析成功,应将其设置为 true,否则设置为 false

现在来看函数体。注意它只是分派到另一个 call() 重载。这非常常见,因为两个重载都需要进行相同的解析;只有属性可能不同。函数体的第一行定义了 attr_t,这是我们包装的解析器 parser_ 的默认属性类型。它通过获取 parser_.call() 使用的 decltype() 来做到这一点。(这表示在文档的其余部分中表示为 ATTR() 的逻辑。)由于 opt_parser 代表一个可选值,其属性的自然类型是 std::optional<ATTR(parser)>。然而,这并不适用于所有情况。特别是,它不适用于“无属性”类型 detail::nope,也不适用于 std::optional<T>ATTR(--p) 只是 ATTR(-p)。因此,第二行使用了一个处理这些细节的别名,即 detail::optional_of<>。第三行只是调用 call() 的另一个重载,将 retval 作为输出参数传递。最后,retval 在最后一行返回。

现在,转向另一个重载。

template<
    typename Iter,
    typename Sentinel,
    typename Context,
    typename SkipParser,
    typename Attribute>
void call(
    Iter & first,
    Sentinel last,
    Context const & context,
    SkipParser const & skip,
    detail::flags flags,
    bool & success,
    Attribute & retval) const
{
    [[maybe_unused]] auto _ = detail::scoped_trace(
        *this, first, last, context, flags, retval);

    detail::skip(first, last, skip, flags);

    if (!detail::gen_attrs(flags)) {
        parser_.call(first, last, context, skip, flags, success);
        success = true;
        return;
    }

    parser_.call(first, last, context, skip, flags, success, retval);
    success = true;
}

这里的模板和函数参数与另一个重载的相同,只是我们有 Attribute &retval,这是我们的输出参数。

让我们一点一点地看一下实现。

[[maybe_unused]] auto _ = detail::scoped_trace(
    *this, first, last, context, flags, retval);

这定义了一个 RAII 跟踪对象,如果用户在顶层解析中传递了 boost::parser::trace::on,它将产生详细的跟踪。它只在 detail::enable_trace(flags)true 时生效。如果启用了跟踪,它将在定义时显示解析状态,并在超出作用域时再次显示。

[Important] 重要提示

为了使跟踪代码正常工作,您必须为您的新解析器类型/模板定义 detail::print_parser 的重载。请参阅 <boost/parser/detail/printing.hpp> 中的示例。

detail::skip(first, last, skip, flags);

这个很简单;它只是应用了跳过解析器。 opt_parser 只有一个子解析器,但如果它有多个子解析器,或者它应用了一个子解析器不止一次,它就需要重复这一行,并在每次使用任何子解析器之间使用 skip

if (!detail::gen_attrs(flags)) {
    parser_.call(first, last, context, skip, flags, success);
    success = true;
    return;
}

这条路径处理我们根本不想生成属性的情况,也许是因为这个解析器位于 omit[] 指令中。

parser_.call(first, last, context, skip, flags, success, retval);
success = true;

这是另一条典型路径。在这里,我们确实想要生成属性,所以我们执行相同的 parser_.call() 调用,只是我们还传递了 retval

请注意,在两种代码路径中,调用 parser_.call() 后,我们都将 success 设置为 true。由于 opt_parser 是零个或一个,因此,即使子解析器失败,opt_parse 仍然会成功。

何时创建新的解析上下文

有时,您需要在调用子解析器之前更改解析上下文的某些内容。例如,rule_parser 设置了可用于该规则的值、局部变量等。action_parser 将生成的属性添加到上下文中(可通过 _attr(ctx) 访问)。Boost.Parser 中的上下文是不可变的。要为子解析器“修改”一个上下文,您需要通过调用 detail::make_context() 来创建一个新的上下文。

detail::apply_parser()

有时,一个解析器需要操作一个与默认属性不完全相同但以某种方式兼容的输出参数。为此,解析器通常需要调用自身,但使用略有不同的参数。detail::apply_parser() 有助于实现这一点。有关示例,请参阅 repeat_parser::call() 的输出参数重载。请注意,由于这会为备用解析器创建一个新的作用域,因此 scoped_trace 对象需要知道我们是否在 detail::apply_parser 内部。

我知道这信息量很大。再次强调,本节并非旨在提供深入的教程。您现在已经了解了足够多的知识,可以至少读懂 parser.hpp 中的解析器了。


PrevUpHomeNext