为什么 std::common_iterator 只是 std::forward_iterator?
Why is std::common_iterator just 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
denotes forward_iterator_tag
if I
models
forward_iterator
; otherwise it denotes input_iterator_tag
.
为什么common_iterator
最多只能是一个forward_iterator
,而不能完全根据I
定义它的iterator_concept
的iterator_category
?例如,如果 I
是 random_asscess_iterator
,那么 common_iterator<I, S>
就是 random_asscess_iterator
,依此类推。
看来这在技术上是可行的,因为common_iterator
只是使用std::variant
来键入擦除I
和S
。
考虑以下 (godbolt):
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
是random_access_range
,所以C++20的约束算法如ranges::binary_search
可以利用这个trait对其进行更高效的操作,但是为了启用旧的std
算法来应用它,我们需要用views::common
把它转换成common_range
。但是,它也退化为forward_range
,这降低了算法的效率。
为什么标准最多只定义common_iterator
为forward_iterator
?这背后的考虑是什么?
哨兵的概念与其他语言中已知的迭代器密切相关,它支持前进并测试您是否到达终点。一个很好的例子是一个以零结尾的字符串,当您到达 [=10=]
但事先不知道大小时就停止了。
我的假设是,将其建模为 std::forward_iterator
足以满足您需要使用哨兵转换 C++20 迭代器以调用旧算法的用例。
我还认为应该可以提供一个通用实现来检测迭代器提供更多功能的情况。它会使标准库中的实现复杂化,也许这就是反对它的理由。在通用代码中,您仍然可以自己检测特殊情况以避免包装随机访问迭代器。
但据我了解,如果您处理性能关键代码部分,除非需要,否则应小心将所有内容包装为 std::common_iterator
。如果底层 variant
引入了一些开销,我不会感到惊讶。
如@daves 所述,结束迭代器是一个迭代器。
如果您的迭代器支持 --
(因为一切都比 forward 更强大),那么您的通用迭代器必须能够 --
从包装的哨兵。
哨兵不支持--
;你会以某种方式伪造它。在许多情况下,这需要 O(n) 的工作量;基本上扫描哨兵并将其变成迭代器。太烂了。
如果你有一个非公共范围的迭代器,你需要将它转换成一个公共范围,那么你基本上有两种选择。
您可以通过连续递增开始迭代器直到它等于哨兵来计算结束迭代器是什么,或者您可以耍花招。 common_iterator
用于 后者 .
这很重要,因为连续递增开始迭代器有两个缺陷。首先,如果它至少不是一个前向射程,你就不能这样做。其次...如果范围是无限会怎样?因为那是 C++20 范围内的事情。事实上,这种可能性是我们拥有哨兵类型的最重要原因之一。
那就太诡异了。但是,在这种情况下,“诡计”意味着创建一个 new 迭代器类型,用于开始和标记。因此,它必须具有两者的限制。哨兵基本上必须假装它是一个迭代器。
迭代器可以递增;哨兵不能。但是,您无论如何都不允许递增范围的结束迭代器,因此您可以假装允许递增结束common_iterator
。同样,您不能取消对哨兵的引用,但也不能取消对结束迭代器的引用。所以它可以假装它可以被取消引用,即使没有算法会那样做。
这意味着对于前向范围,您不能做任何事情 除了 将结束迭代器与其他迭代器进行测试。简而言之,对于前向范围,结束迭代器也可以是哨兵。
但对于更高级别的范围而言,情况并非如此。明确允许采用双向范围的算法向后移动结束迭代器。但是你不能对假装它是迭代器的哨兵这样做。
这就是为什么 common_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
denotesforward_iterator_tag
ifI
modelsforward_iterator
; otherwise it denotesinput_iterator_tag
.
为什么common_iterator
最多只能是一个forward_iterator
,而不能完全根据I
定义它的iterator_concept
的iterator_category
?例如,如果 I
是 random_asscess_iterator
,那么 common_iterator<I, S>
就是 random_asscess_iterator
,依此类推。
看来这在技术上是可行的,因为common_iterator
只是使用std::variant
来键入擦除I
和S
。
考虑以下 (godbolt):
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
是random_access_range
,所以C++20的约束算法如ranges::binary_search
可以利用这个trait对其进行更高效的操作,但是为了启用旧的std
算法来应用它,我们需要用views::common
把它转换成common_range
。但是,它也退化为forward_range
,这降低了算法的效率。
为什么标准最多只定义common_iterator
为forward_iterator
?这背后的考虑是什么?
哨兵的概念与其他语言中已知的迭代器密切相关,它支持前进并测试您是否到达终点。一个很好的例子是一个以零结尾的字符串,当您到达 [=10=]
但事先不知道大小时就停止了。
我的假设是,将其建模为 std::forward_iterator
足以满足您需要使用哨兵转换 C++20 迭代器以调用旧算法的用例。
我还认为应该可以提供一个通用实现来检测迭代器提供更多功能的情况。它会使标准库中的实现复杂化,也许这就是反对它的理由。在通用代码中,您仍然可以自己检测特殊情况以避免包装随机访问迭代器。
但据我了解,如果您处理性能关键代码部分,除非需要,否则应小心将所有内容包装为 std::common_iterator
。如果底层 variant
引入了一些开销,我不会感到惊讶。
如@daves 所述,结束迭代器是一个迭代器。
如果您的迭代器支持 --
(因为一切都比 forward 更强大),那么您的通用迭代器必须能够 --
从包装的哨兵。
哨兵不支持--
;你会以某种方式伪造它。在许多情况下,这需要 O(n) 的工作量;基本上扫描哨兵并将其变成迭代器。太烂了。
如果你有一个非公共范围的迭代器,你需要将它转换成一个公共范围,那么你基本上有两种选择。
您可以通过连续递增开始迭代器直到它等于哨兵来计算结束迭代器是什么,或者您可以耍花招。 common_iterator
用于 后者 .
这很重要,因为连续递增开始迭代器有两个缺陷。首先,如果它至少不是一个前向射程,你就不能这样做。其次...如果范围是无限会怎样?因为那是 C++20 范围内的事情。事实上,这种可能性是我们拥有哨兵类型的最重要原因之一。
那就太诡异了。但是,在这种情况下,“诡计”意味着创建一个 new 迭代器类型,用于开始和标记。因此,它必须具有两者的限制。哨兵基本上必须假装它是一个迭代器。
迭代器可以递增;哨兵不能。但是,您无论如何都不允许递增范围的结束迭代器,因此您可以假装允许递增结束common_iterator
。同样,您不能取消对哨兵的引用,但也不能取消对结束迭代器的引用。所以它可以假装它可以被取消引用,即使没有算法会那样做。
这意味着对于前向范围,您不能做任何事情 除了 将结束迭代器与其他迭代器进行测试。简而言之,对于前向范围,结束迭代器也可以是哨兵。
但对于更高级别的范围而言,情况并非如此。明确允许采用双向范围的算法向后移动结束迭代器。但是你不能对假装它是迭代器的哨兵这样做。
这就是为什么 common_iterator
范围不能高于前向范围的原因。