问题描述
取自 range-v3 文档,以下示例演示了 views
的简单组合,通过流水线生成 range
:
std::vector<int> const vi{1,2,3,4,5,6,7,8,9,10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){return i % 2 == 1;})
| views::transform([](int i){return std::to_string(i);});
我知道 views::foo
等价于 foo_view()
之类的东西,因此上面的例子最终是这样的:
transform_view(remove_if_view(vi,<lambda>),<lambda>)
现在的问题是:
remove_if
和 transform
操作的顺序是如何发生的? (我知道它们是懒惰的,它们实际上并没有在这一步计算,而不是在 rng
实现时计算,但这不是重点。
我可以在这里看到两个选项:
这些操作由 range-v3 融合,当
rng
的给定元素通过某个迭代器访问时,这两个操作因此应用于该元素。 >-
当请求给定元素时,整个
remove_if
操作会应用于整个vi
,然后该操作的输出向量被送入transform
。因此,我们最终得到了一个完整的“trasformed + removed_if”向量,它使我们能够访问所需的元素。
我很确定选项 (1) 是实际发生的情况。如果是这种情况,range-v3 是如何实现的?它是否有某种通用组合代码来组合无限数量的组合视图操作?
附带问题:range-v3 视图公开什么样的迭代器?我认为 random-access
迭代器下面的任何内容都会使并行化变得不可能。
元问题:如果选项 (1) 是事实,那么并行化 range-algorithms
是不是非常简单,因为它们将一个简单的范围(由多个视图组成,按需计算)作为输入操作融合)?
解决方法
如果我不得不猜测,那么我会说 |
运算符构建了一个编译时 AST(抽象语法树)操作。如果将此 AST 存储到名为 range
的变量中,然后调用 auto it = std::begin(range)
,则您将实现一个迭代器,该迭代器的类型将非常重要(除非使用类型擦除)。当您调用 *it
时,它会评估从取消引用原始迭代器的当前状态开始的所有转换操作。当您调用 ++it
时,它有点复杂。它需要做几件事。它需要推进基本迭代器,但它还需要评估所有中间转换,以便可以评估 remove_if
中的谓词。如果谓词返回 true,则基迭代器需要再次前进,因为我们正在跳过一个项目。
如果您将 transform
放在 remove_if
之前,这可能会导致管道效率低下。对于每个项目,转换将被评估两次。一次用于推进迭代器,第二次用于读取当前值。如果转换不是微不足道的,那么你会减速。如果转换有副作用,那么可能会发生不好的事情。见
了解更多详情
关于使操作并行,它可能类似于 std 库实现,通过向每个 AST 节点添加一个标签参数来说明您是否要并行执行。有关详细信息,请参阅https://www.modernescpp.com/index.php/parallel-algorithm-of-the-standard-template-library
例如
vector<int> v = ...
// standard sequential sort
std::sort(v.begin(),v.end());
// sequential execution
std::sort(std::parallel::seq,v.begin(),v.end());
// permitting parallel execution
std::sort(std::parallel::par,v.end());
// permitting parallel and vectorized execution
std::sort(std::parallel::par_unseq,v.end());
这种模式可以扩展到 range-v3,但本质上重载将是新的实现并且实现起来并不容易。
rangev3 github 页面中有一个关于此主题的未解决问题,其中包含一些您可能感兴趣的进一步链接。
https://github.com/ericniebler/range-v3/issues/921
(我不是 range-v3 的贡献者,但我已经实现了我自己的 range like library,它在功能上与 range-v3 有重叠。)