为什么 ranges::basic_istream_view::begin() 没有被缓存?

Why is ranges::basic_istream_view::begin() not cached?

我发现c++20 ranges::basic_istream_view is slightly different from the range-v3版本。

最重要的区别是 std::ranges::basic_istream_view 不缓存其 begin(),因此每个 begin()s 将 return 具有以下值的下一个迭代器已阅读 (godbolt):

auto words = std::istringstream{"today is yesterday's tomorrow"};
auto view = std::ranges::istream_view<std::string>(words);
std::cout << *view.begin() << "\n"; // today
std::cout << *view.begin() << "\n"; // is
std::cout << *view.begin() << "\n"; // yesterday's
std::cout << *view.begin() << "\n"; // tomorrow

考虑以下(godbolt),如果我使用range-v3版本,所有三个std::ranges::find()都会找到"is",但如果我使用std版本, "is" 只会在第一次调用时被发现。

auto words = std::istringstream{"today is yesterday's tomorrow"};
auto view = std::ranges::istream_view<std::string>(words);
std::cout << *std::ranges::find(view, "is") << "\n"; // is
std::cout << *std::ranges::find(view, "is") << "\n"; // tomorrow
std::cout << *std::ranges::find(view, "is") << "\n"; // tomorrow

为什么标准选择了与 range-v3 不同的设计? begin()被缓存是否有潜在的缺陷?

输入迭代器是这样的,一旦你取消引用一个,你需要立即增加它。输出迭代器也是如此,例如 back_insert_iterator。这是你不应该做的事情。如果您需要缓存第一个值,请自行缓存。

输入和输出迭代器在取消引用后需要递增的原因是它们在设计上是单一的pass.If你已经从流中读取了一些东西,你不能再次读取它。运算符 * 实际上是从流中读取的。 ++是做什么的?没有什么! back_insert_iterator

也一样

range概念的定义中,在[range.range]中,我们有:

template<class T>
  concept range =
    requires(T& t) {
      ranges::begin(t);   // sometimes equality-preserving (see below)
      ranges::end(t);
    };

“见下文”部分在哪里(强调我的):

Given an expression t such that decltype((t)) is T&, T models range only if

  • ...
  • if the type of ranges​::​begin(t) models forward_­iterator, ranges​::​begin(t) is equality-preserving.

[Note 1: Equality preservation of both ranges​::​begin and ranges​::​end enables passing a range whose iterator type models forward_­iterator to multiple algorithms and making multiple passes over the range by repeated calls to ranges​::​begin and ranges​::​end. Since ranges​::​begin is not required to be equality-preserving when the return type does not model forward_­iterator, it is possible for repeated calls to not return equal values or to not be well-defined. — end note]

对于正向范围,您可以重复调用 ranges::begin(r) 并期待相同的答案(它是相等的)。对于某些范围,这需要缓存。

但对于仅输入范围(如 istream_view),您可以只调用一次 ranges::begin(r),并且无法保证第二次调用时会发生什么。

因此,有效程序无法观察到这两种实现之间的差异,因为多次调用 begin 已经违反了这里的先决条件。


关于 istream_view 的更具体的答案,range-v3 的实现也不缓存 begin()。这不是正在发生的差异。事实上,无论如何你都不能缓存输入迭代器,因为它们会立即失效。

不同之处在于 我们从流中读取第一个值时:

  • range-v3 中,这发生在 istream_view 的构造函数中。
  • libstdc++ 中,这发生在对 begin().
  • 的调用中

后者更符合一般范围模型,在该模型中构建范围适配器实际上不做任何工作。