The multi pass iterator - 1.87.0
Multi-Pass 迭代器
Spirit.Qi 的回溯与迭代器类型要求
在 Boost.Spirit.Qi 中,解析回溯需要以下几种迭代器类型之一:前向迭代器(forward iterator)、双向迭代器(bidirectional iterator) 或 随机访问迭代器(random access iterator)。由于回溯需要保存迭代器的当前位置,输入迭代器(input iterator) 无法使用。因此,标准库中的 std::istreambuf_iterator
和 std::istream_iterator
都不能直接应用于 Spirit.Qi。
另一种需要注意的输入迭代器是用于包装词法分析器(lexer)的迭代器,例如 LEX。
注意事项
Spirit.Qi 通常生成递归下降解析器,其设计上依赖回溯功能。因此,至少需要为其 API 提供 前向迭代器。不过,这并非绝对必要。未来可能会出现更加确定性的解析器,它们只需 1 个字符(或 token)作为前瞻即可工作。这类解析器可以直接使用输入迭代器(如 std::istream_iterator
)。
输入迭代器的限制
回溯的实现依赖于保存当前迭代器的位置,即对迭代器进行拷贝。然而,输入迭代器不支持位置保存,因此无法用于 Spirit.Qi 的回溯需求。
一种解决方法是将所有需要解析的数据预先加载到一个容器(如 std::vector
或 std::deque
)中,然后将容器的起始和结束迭代器传递给 Spirit.Qi。但这种方法可能会对内存造成较大压力,这也是 multi_pass
迭代器被创建的原因。
使用 Multi-Pass
Multi-Pass 的作用
multi_pass
迭代器可以将任何输入迭代器转换为适合 Spirit.Qi 使用的前向迭代器。它会根据需要缓冲数据,并在不再需要时丢弃缓冲区的内容。具体情况包括:
- 只有一个迭代器副本存在时。
- 不可能发生回溯时。
使用 Multi-Pass 时的语法设计
当使用 multi_pass
时,需要谨慎设计解析语法(grammar)。例如:
- 可能触发回溯的规则(如包含选择项的规则)会导致数据被缓冲。
- 重复性规则(如 Kleene 星号
*
和加号+
)是更高效的选择。
序列规则与缓冲
形如 a >> b
的序列规则也会导致数据缓冲。
这种行为与 Spirit.Classic 的行为不同,但有充分理由:
在序列中,如果某个组件匹配失败,解析器需要将当前迭代器重置为初始状态。
期望点规则(expectation points)
为了优化这种行为,期望点规则(a > b
)被引入到解析器中。期望点在语法中引入了确定性点,确保匹配成功时不会发生回溯。在每个期望点,multi_pass
迭代器会清空其缓冲区,从而保证即使是大型语法,缓冲内容也能保持最小化。
重要提示
如果你在使用 multi_pass
时结合了期望点规则和错误处理器,并希望错误处理器强制重试或失败(如 on_error<retry>
或 on_error<fail>
),则需要明确地以 retry
或 fail
的形式实例化错误处理器。例如:
rule r<iterator_type> r;
on_error<retry>(r, std::cout << phoenix::val("Error!"));
如果未按照上述方式处理,则运行时会触发断言错误。
重复性规则的缓冲优化
对于重复性规则(如 *a
或 +a
),multi_pass
迭代器只会缓冲当前重复的内容。
在典型的语法中,解析的歧义(即需要前瞻)通常是局部化的。实际上,许多设计良好的语言是完全确定性的(LL(1) 语法),无需前瞻。例如,只需查看输入的第一个字符(或 token),即可决定解析的分支。
即使对于高度歧义的语法,选择项通常也呈现如下形式:
*(a | b | c | d)
此时,输入迭代器会继续前进,而不会卡在开头。例如,以下是 Pascal 中的一段语法:
program =programHeading >> block >> '.';block =*( labelDeclarationPart| constantDefinitionPart| typeDefinitionPart| variableDeclarationPart| procedureAndFunctionDeclarationPart)>> statementPart;
注意,在规则 block
中,Kleene 星号中的选择项会以线性方式解析输入,并在每次迭代中丢弃历史数据。由于这是一个完全确定性的 LL(1) 语法,每个失败的选择项只需查看 1 个字符(或 token)即可决定是否前进。
Flush Multi-Pass
如果语法是确定性的,可以使用 flush_multi_pass
伪解析器来避免不必要的缓冲区。例如:
- 在 Spirit.Qi 的解析器库中,可以使用
flush_multi_pass
来优化缓冲行为。
示例:解析浮点数列表
以下是一个使用 multi_pass
的最小示例:
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/support_multi_pass.hpp>
#include <fstream>
#include <iostream>
#include <vector>int main()
{namespace spirit = boost::spirit;using spirit::ascii::space;using spirit::ascii::char_;using spirit::qi::double_;using spirit::qi::eol;std::ifstream in("multi_pass.txt"); // 从文件获取输入if (!in.is_open()) {std::cout << "无法打开输入文件: 'multi_pass.txt'" << std::endl;return -1;}typedef std::istreambuf_iterator<char> base_iterator_type;spirit::multi_pass<base_iterator_type> first =spirit::make_default_multi_pass(base_iterator_type(in));std::vector<double> v;bool result = spirit::qi::phrase_parse(first,spirit::make_default_multi_pass(base_iterator_type()),double_ >> *(',' >> double_), // 识别浮点数列表space | '#' >> *(char_ - eol) >> eol, // 跳过注释v);if (!result) {std::cout << "解析输入文件失败!" << std::endl;return -2;}std::cout << "成功解析输入文件!" << std::endl;return 0;
}
此示例展示了如何使用 multi_pass
从文件中解析浮点数列表。multi_pass
提供了默认的策略配置,使其能够轻松与 std::istreambuf_iterator
等输入迭代器结合使用。
使用 flush_multi_pass 解析器
flush_multi_pass
解析器是 Spirit 仓库中的一个组件,它与 multi_pass
迭代器结合使用,可以最小化缓冲需求。通过在语法中插入显式的同步点,确保在这些点之后不会发生回溯,从而可以安全地清除存储的输入数据。
当 flush_multi_pass
与 multi_pass
配合使用时,会调用 multi_pass::clear_queue()
,清除任何已缓冲的数据。这将使所有其他 multi_pass
的副本失效,使用这些副本会引发 boost::illegal_backtracking
异常。
multi_pass 策略
multi_pass 的配置
multi_pass
是一个支持策略配置的模板类。上述描述的 multi_pass
行为是其最初实现(没有策略时的默认配置),并且现在仍作为默认行为。然而,multi_pass
支持更多扩展功能。由于策略的开放性,可以通过自定义策略来扩展 multi_pass
的功能。
multi_pass 的模板参数
-
Input
定义multi_pass
获取输入的方式。通常是一个输入迭代器或函数对象。 -
Policies
定义用于创建multi_pass
迭代器实例的组合策略类型。组合策略由以下四种功能策略组成:
default_policy 模板所需的策略
multi_pass
使用模板类 iterator_policies::default_policy
来组合以下四种不同的策略,每种策略负责一种功能:
模板参数 | 描述 |
---|---|
OwnershipPolicy | 决定 multi_pass 如何管理其共享组件。 |
CheckingPolicy | 决定如何进行无效迭代器的检查。 |
InputPolicy | 定义 multi_pass 如何获取输入,参数化为 Input 模板参数。 |
StoragePolicy | 决定并管理 multi_pass 的缓冲方案。 |
预定义的策略
multi_pass
库为每种策略类型提供了若干预定义实现,均位于 boost::spirit::iterator_policies
命名空间中:
预定义策略类
- InputPolicy 类
类名 | 描述 |
---|---|
input_iterator | 指示 multi_pass 从 Input 类型的输入迭代器中读取数据。 |
buffering_input_iterator | 类似于 input_iterator ,但会缓冲从底层迭代器读取的最后一个字符(如 std::istreambuf_iterator )。 |
istream | 指示 multi_pass 从 Input 类型的输入流(通常是 std::basic_istream )中读取数据。 |
lex_input | 通过调用 yylex() 获取输入,通常由 Flex 生成的扫描器提供。需要链接到 Flex 生成的扫描器。 |
functor_input | 通过调用 Input 类型的函数对象获取数据。函数对象需包含 result_type 和 eof 静态变量。 |
split_functor_input | 类似于 functor_input ,但分离了唯一和共享的子类,可以整合其数据成员与 multi_pass 的数据。 |
- OwnershipPolicy 类
类名 | 描述 |
---|---|
ref_counted | 使用引用计数机制,在引用计数归零时释放共享组件。 |
first_owner | 第一个创建的 multi_pass 拥有共享数据,其它副本不拥有数据。适合于栈分配的迭代器。 |
- CheckingPolicy 类
类名 | 描述 |
---|---|
no_check | 不进行任何检查。 |
buf_id_check | 使用缓冲区 ID,每次调用 clear_queue() 时更新缓冲区 ID,并在迭代器解引用时检查一致性。适用于 split_std_deque 。 |
full_check | (尚未实现)未来将跟踪所有迭代器以确保它们有效,适合调试使用但开销较大。 |
- StoragePolicy 类
类名 | 描述 |
---|---|
split_std_deque | 使用 std::vector 存储所有缓冲数据,适合需要动态缓冲的场景。 |
fixed_size_queue<N> | 使用固定大小的环形缓冲区,缓冲区大小为 N+1 ,适合对内存需求受限的场景。 |
自定义策略
尽管 multi_pass
提供了丰富的预定义策略,但也可以根据需求实现自己的策略。以下是一些自定义策略的设计指南:
-
输入策略(InputPolicy)
确保定义清晰的输入获取机制和终止条件(如eof
变量)。 -
存储策略(StoragePolicy)
平衡内存使用和性能需求。例如,如果数据较大且解析复杂,可以选择动态缓冲策略;如果数据较小且固定,可以选择固定缓冲策略。 -
检查策略(CheckingPolicy)
根据调试需求选择合适的检查策略。例如,开发阶段可以使用严格的检查策略(如buf_id_check
),而在生产阶段可以选择no_check
以减少开销。
使用自定义 multi_pass
的方法
Boost.Spirit 提供了一种基于策略的设计方式,通过组合各种策略,可以灵活定制 multi_pass
迭代器的行为。以下是关于自定义 multi_pass
的关键点和步骤:
1. 定义自定义的 multi_pass
类型
以下是一个基于 std::istream_iterator<char>
的示例,比默认的 multi_pass
更高效:
typedef multi_pass<std::istream_iterator<char>,iterator_policies::default_policy<iterator_policies::first_owner, // 所有权策略iterator_policies::no_check, // 检查策略iterator_policies::buffering_input_iterator, // 输入策略iterator_policies::split_std_deque // 存储策略>
> first_owner_multi_pass_type;
此定义的 multi_pass
使用了 iterator_policies::first_owner
和 iterator_policies::no_check
策略,从而略微提高了效率。
2. 默认参数解释
默认情况下,如果使用 multi_pass<std::istream_iterator<char>>
,它将采用以下默认策略:
- 所有权策略:
iterator_policies::ref_counted
- 检查策略:
iterator_policies::no_check
(若定义了BOOST_SPIRIT_DEBUG
,则为iterator_policies::buf_id_check
) - 输入策略:
iterator_policies::buffering_input_iterator
- 存储策略:
iterator_policies::split_std_deque
3. 处理固定的前瞻量
look_ahead
是一个预定义的类,提供固定大小的前瞻功能。它适用于对性能要求更高的场景:
typedef look_ahead<std::istream_iterator<char>, 10> look_ahead_iterator;
- 第二个模板参数
10
指定缓冲区大小。 - 适合需要快速解析固定前瞻量的场景。
4. 从标准输入流读取
basic_istream_iterator
是一个可以替代 std::istream_iterator
的前向迭代器。与 std::istream_iterator
不同,它是前向迭代器而非输入迭代器:
typedef basic_istream_iterator<char, std::char_traits<char>> istream_iterator;
此迭代器提供更强的功能,并且可以用于标准输入流的封装。
....