您知道这是否是正确的预期行为(以及为什么)?
是的:这是预期的行为。这是迭代模型的固有属性,我们有operator*
and operator++
作为单独的操作。
filter
's operator++
必须寻找下一个满足谓词的底层迭代器。这涉及到做*it
on transform
的迭代器涉及调用该函数。但是一旦我们找到下一个迭代器,当我们再次读取它时,就会再次调用转换。在代码片段中:
decltype(auto) transform_view<V, F>::iterator::operator*() const {
return invoke(f_, *it_);
}
decltype(auto) filter_view<V, P>::iterator::operator*() const {
// reading through the filter iterator just reads
// through the underlying iterator, which in this
// case means invoking the function
return *it_;
}
auto filter_view<V, P>::iterator::operator++() -> iterator& {
for (++it_; it_ != ranges::end(parent_->base_); ++it_) {
// when we eventually find an iterator that satisfies this
// predicate, we will have needed to read it (which calls
// the functions) and then the next operator* will do
// that same thing again
if (invoke(parent_->pred_, *it_))) {
break;
}
}
return *this;
}
结果是我们对满足谓词的每个元素调用该函数两次。
解决方法是要么不关心(让转换足够便宜,调用它两次并不重要,或者过滤器足够稀有,使得重复转换的数量无关紧要,或者两者兼而有之),或者添加一个缓存层你的管道。
C++20 Ranges 中没有缓存视图,但 range-v3 中有一个名为views::cache1
:
ranges::views::iota(0)
| ranges::views::transform(f)
| ranges::views::cache1
| ranges::views::filter(g)
这确保了f
每个元素最多只被调用一次,代价是必须处理元素缓存并将范围降级为仅输入范围(之前是双向的)。