为什么 std::common_iterator 只是 std::forward_iterator?

问题描述

C++20 引入了一个 std::common_iterator,它能够将元素的非公共范围(迭代器和标记的类型不同)表示为公共范围(它们相同),它的概要定义为:

template<input_­or_­output_­iterator I,sentinel_­for<I> S>
    requires (!same_­as<I,S> && copyable<I>)
class common_iterator {
  // ...
 private:
  variant<I,S> v_;   // exposition only
};

它对于与期望范围的开始和结束具有相同类型的遗留代码进行交互非常有用。

[iterators.common#common.iter.types-1.1]中,它的iterator_­concept定义为:

iterator_­concept 表示 forward_­iterator_­tag 如果 I 模型 forward_­iterator;否则表示 input_­iterator_­tag

为什么common_iterator最多只能是一个forward_iterator,并且不能完全根据iterator_concept的{​​{1}定义它的I }}?例如,如果 iterator_categoryI,则 random_asscess_iteratorcommon_iterator<I,S>,依此类推。

这似乎在技术上是可行的,因为 random_asscess_iterator 只是使用 common_iterator 来键入擦除 std::variantI

考虑以下 (godbolt):

S

auto r = views::iota(0) | std::views::take(5); static_assert( ranges::random_access_range<decltype(r)>); auto cr = r | views::common; static_assert(!ranges::random_access_range<decltype(cr)>); static_assert( ranges::forward_range<decltype(cr)>); r,因此 C++20 约束算法如 random_access_range 可以使用此特征对其执行更有效的操作,但为了启用旧的 {{ 1}} 算法来应用它,我们需要使用 ranges::binary_search 将其转换为 std。但是,它也退化为views::common,降低了算法的效率。

为什么标准最多只将common_range定义为forward_range?这背后的考虑是什么?

解决方法

如果你有一个非通用范围的迭代器,你需要将它转换成一个通用范围,那么你基本上有两种选择。

您可以通过连续递增开始迭代器直到它等于哨兵来计算结束迭代器,或者您可以做一些技巧。 common_iterator 用于 后者

这很重要,因为连续递增开始迭代器有两个缺陷。首先,如果它不是至少一个前锋范围,你就不能这样做。其次......如果范围是无限会发生什么?因为那是 C++20 范围内的事情。事实上,这种可能性是我们拥有哨兵类型的最重要原因之一。

那么诡计多端。然而,在这种情况下,“诡计”意味着创建一个 new 迭代器类型,用于开始和标记。因此,它必须具有两者的局限性。哨兵基本上必须假装它是一个迭代器。

迭代器可以递增;哨兵不能。但是,无论如何您都不允许增加范围的结束迭代器,因此您可以假装增加结束 common_iterator 是允许的。同样,您不能取消引用哨兵,但也不能取消引用结束迭代器。所以它可以假装它可以被取消引用,即使没有算法会这样做。

这意味着对于前向范围,您不能做任何事情除了针对其他迭​​代器测试结束迭代器。简而言之,对于前向范围,结束迭代器也可能是一个哨兵。

但对于更高级别的范围,情况并非如此。明确允许采用双向范围的算法将结束迭代器向后移动。但是你不能对假装它是迭代器的哨兵这样做。

这就是为什么 common_iterator 范围不能高于前向范围的原因。

,

正如@daves 提到的,结束迭代器是一个迭代器。

如果您的迭代器支持 --(因为一切都比 forward 更强大),那么您的通用迭代器必须能够从包装的哨兵中--

哨兵不支持--;你会以某种方式伪造它。在许多情况下,这需要 O(n) 的工作;基本上扫描哨兵并将其转换为迭代器。这很糟糕。

,

哨兵的概念与迭代器密切相关,因为它在其他语言中是众所周知的,它支持前进并测试您是否到达终点。一个很好的例子是一个以零结尾的字符串,当您到达 \0 时停止,但事先不知道大小。

我的假设是,将其建模为 std::forward_iterator 足以满足需要转换带有标记的 C++20 迭代器来调用旧算法的用例。

我还认为应该可以提供一个通用的实现来检测迭代器提供更多功能的情况。它会使标准库中的实现复杂化,也许这就是反对它的论据。在通用代码中,您仍然可以自己检测特殊情况,以避免包装随机访问迭代器。

但据我所知,如果您处理性能关键的代码部分,除非需要,否则应小心将所有内容包装为 std::common_iterator。如果底层 variant 引入一些开销,我不会感到惊讶。