c++ - 生成器在 C++20 View 管道中调用两次

标签 c++ c++20 range-v3 std-ranges

这里是一个简单的管道 views适配器,有 gen调用函数以生成一系列值(使用内部状态),然后对其进行过滤。
令人惊讶和违反直觉的(至少对我而言)是这样一个事实,即每次迭代都会调用生成器函数两次,因此对同一过滤器的下一次检查失败(过滤后的值不会在管道中重用)。
您知道这是否是正确的预期行为(以及为什么)?
libstdc++ 测试在 GCC 10.3、11.1 和主干 ( code ) 和 range-v3 中使用 GCC 和 clang ( code )。

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;
  };
(我知道纯函数更好)

最佳答案

Do you have an idea if this is the correct expected behavior (and why)?


是:这是预期的行为。这是迭代模型的固有属性,我们有 operator*operator++作为单独的操作。filteroperator++必须寻找下一个满足谓词的底层迭代器。这涉及到做 *ittransform的迭代器,它涉及调用函数。但是一旦我们找到下一个迭代器,当我们再次阅读它时,它将再次调用转换。在代码片段中:
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 中没有缓存 View ,但 range-v3 中有一个名为 views::cache1 :
ranges::views::iota(0)
    | ranges::views::transform(f)
    | ranges::views::cache1
    | ranges::views::filter(g)
这确保了 f每个元素最多只能调用一次,代价是必须处理元素缓存并将范围降级为仅输入范围(之前它是双向的)。

关于c++ - 生成器在 C++20 View 管道中调用两次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67321666/

相关文章:

c++ - 整数的模数

c++ - macOS 使用 CMake 构建通用二进制 2

c++ - 为什么 C++ 标准不更改 std::set 以使用 std::less<> 作为其默认模板参数?

c++ - libfmt 和 std::format 之间有什么区别?

c++ - 为什么内存_order_release直到C++20才支持?

c++ - Range-v3 中 Readable 使用的 CommonReference 有什么作用?

c++ - 将 LEVEL 包含到 SysLog 日志文件中

c++ - 在 GitHub 上编译 TinyMT 时隐式声明的 ‘...’ 已弃用 [-Wdeprecated-copy]

c++ - 如何使用 range-v3 获取集合的所有权?

c++ - range-v3 中的 view_closure 是什么?