问题描述
在一个简单的 views
适配器管道中,调用了 gen
函数以生成一系列值(使用内部状态),然后对其进行过滤。
令人惊讶和违反直觉的(至少对我而言)是生成器函数在每次迭代中被调用两次,因此对同一过滤器的下一次检查失败(过滤后的值不会在管道中重用)。
您知道这是否是正确的预期行为(以及为什么)?
使用 GCC 10.3、11.1 和主干 (code) 中的 libstdc++
以及 GCC 和 clang (code) 中的 range-v3
进行测试。
int main() {
int n = 0;
auto gen = [&n]() {
auto result = ++n;
std::cout << "Generate [" << result << "]\n";
return result;
};
auto tmp =
ranges::views::iota(0)
| ranges::views::transform([gen](auto &&) { return gen(); })
| ranges::views::filter([](auto &&i) {
std::cout << "#1 " << i << " " << (i % 2) << "\n";
return (i % 2) == 1;
});
for (auto &&i : tmp | ranges::views::take(1)) {
std::cout << "#2 " << i << " " << ((i % 2) == 1) << "\n";
assert(((i % 2) == 1));
}
}
注意:如果 gen
函数被编写为具有内部状态的可变函数,则它不会编译:
auto gen = [n=0]() mutable {
auto result = ++n;
std::cout << "Generate [" << result << "]\n";
return result;
};
(我知道纯函数更好)
解决方法
您知道这是否是正确的预期行为(以及为什么)?
是:这是预期的行为。这是迭代模型的固有属性,其中我们将 operator*
和 operator++
作为单独的操作。
filter
的 operator++
必须寻找下一个满足谓词的底层迭代器。这涉及在 *it
的迭代器上执行 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
每个元素最多只被调用一次,代价是必须处理元素缓存并将您的范围降级为仅作为输入范围(之前它是双向的)。