让我参考关于解析的boost精神教程中的the following example
一个"迷你XML"数据结构。我的问题实际上并没有任何关系
精神,它真正关于boost::variant
,并提高效率"递归"变种
结构。
以下是代码:
struct mini_xml;
typedef
boost::variant<
boost::recursive_wrapper<mini_xml>
, std::string
>
mini_xml_node;
struct mini_xml
{
std::string name; // tag name
std::vector<mini_xml_node> children; // children
};
在教程中,他们继续展示如何适应&#34; struct mini_xml
为boost::fusion
,
然后编写将数据加载到其中的精神语法。
然而,最近我发现在这个例子中存在一个微妙的问题,可能会导致很大的开销。
问题是variant
类型mini_xml_node
在这个例子中不是无法移动的构造。原因是它包含boost::recursive_wrapper
。 recursive_wrapper<T>
始终表示堆分配的T
实例,并且它没有空状态。当一个
移动了包含variant
的{{1}},新的动态分配和新的recursive_wrapper
是从旧T
构建的移动 - 我们不能简单地取得旧T
的所有权,因为这会使旧变体处于空状态。 (在我尝试实现自己的变体类型之后,我只调查了这一点 - 这确实是T
所做的事情,并且在这个例子中确实不是无法移动的构造。)
因为boost::variant
不是no-throw move可构造的,所以容器mini_xml_node
将位于&#34;慢速路径&#34; - 当它使用std::vector<mini_xml_node> children
时,它会选择复制元素
而不是移动它们。例如,每次矢量容量增加时都会发生这种情况。当我们复制std::move_if_noexcept
时,我们会复制其所有孩子。
因此,例如,如果我想从磁盘解析一个深度为mini_xml_node
和分支因子d
的xml树,我估计我们最终将复制树的每个叶子{{{ 1}}次,因为推回到向量b
次导致重新分配大约d * log b
次,并且它将发生在叶子的每个祖先。
我现在没有真正的应用程序我关心这个开销,但我很容易想象 我可能。例如,我可能想要使用精神为一些小型实用程序编写一个高性能的解析器 这将检查,例如数据文件具有某种形式,或者计算某种形式的统计数据。然后呢 很可能是解析时间占据了实用程序的整体运行时间,因此这些副本可能对性能至关重要。
问题是,更改代码以防止这些副本发生的最简单,最干净的方法是什么?我想到了一些方法 但也许你看到了更好的方式。
快速而脏:编写一个包含b
的包装类,但显式标记了移动构造函数
log b
,即使它不是mini_xml_node
。如果抛出异常,这将导致终止,但这应该是非常罕见的......
使用不noexcept
的替代noexcept
,而只使用std::vector
。如果我理解正确,std::move_if_noexcept
会这样做。权衡是它没有强大的异常安全性,但不清楚这是否真的存在问题。
(这个实际上并不奏效。)添加move
作为boost::vector
的选项之一。
当boost::blank
是其中一种值类型时,mini_xml_node
具有适应其行为的特殊代码 - 它将使用
blank
在默认构建和进行类型更改分配时作为自然空状态。
然而,将boost::variant
放入blank
似乎并没有让我无法投掷移动构造
变体,即使它包含blank
类型。使用提升boost::variant
在coliru上进行测试:
http://coliru.stacked-crooked.com/a/87620e443470b70c
(也许这对将来boost::recursive_wrapper
来说是一个有用的功能?我想要的是移动构造函数转移递归包装的所有者的所有权,并将旧的变体放在空白处state,并将移动构造函数标记为1.63
。)