好的,我们有两个相似但不同的概念。所以让我们把它们摆出来。
但首先,我需要区分 C++-pre-20 的命名要求和为 Ranges TS 创建并包含在 C++20 中的实际语言概念。它们都被称为“概念”,但它们的定义不同。因此,当我谈论带有小写字母 c 的概念时,我指的是 C++20 之前的要求。当我谈论 Concept-with-a-captial-C 时,我指的是 C++20 的东西。
代理迭代器
代理迭代器是迭代器,它们reference
不是 a value_type&
,而是一些其他类型,其行为类似于对 的引用value_type
。在这种情况下,*it
返回一个纯右值到 this reference
。
InputIterator 概念对 没有任何要求reference
,只是它可以转换为value_type
。但是,ForwardIterator 概念明确声明“reference
是对”的引用T
。
因此,代理迭代器不能符合 ForwardIterator 的概念。但它仍然可以是 InputIterator。因此,您可以安全地将代理迭代器传递给任何只需要 InputIterators 的函数。
所以,vector<bool>
s 迭代器的问题不在于它们是代理迭代器。这是他们承诺他们实现了 RandomAccessIterator 概念(尽管使用了适当的标签),但实际上它们只是 InputIterators 和 OutputIterators。
C++20 中采用的 Ranges 提议(大部分)对迭代器概念进行了更改,允许代理迭代器适用于所有迭代器。所以在 Ranges 下,vector<bool>::iterator
真正实现了 RandomAccessIterator 的概念。因此,如果您有针对 Ranges 概念编写的代码,那么您可以使用各种代理迭代器。
这对于处理诸如计数范围之类的事情非常有用。你可以拥有reference
并且value_type
是相同的类型,所以你只是在处理整数。
当然,如果你可以控制使用迭代器的代码,你可以让它做你想做的任何事情,只要你不违反你的迭代器所针对的概念。
存储迭代器
存储迭代器是迭代器,其中reference_type
(直接或间接)对存储在迭代器中的对象的引用。因此,如果您制作一个迭代器的副本,则该副本将返回对与原始对象不同的对象的引用,即使它们引用相同的元素。当你增加迭代器时,以前的引用不再有效。
通常会实现存储迭代器,因为计算要返回的值很昂贵。也许它会涉及内存分配(例如path::iterator
),或者它可能会涉及一个可能很复杂的操作,该操作应该只执行一次(例如regex_iterator
)。所以你只想在必要时这样做。
ForwardIterator 作为一个概念(或概念)的基础之一是这些迭代器的范围表示独立于其迭代器存在的值的范围。这允许多通道操作,但它也使做其他事情变得有用。您可以存储对范围内项目的引用,然后在其他地方迭代。
如果您需要将迭代器设为 ForwardIterator 或更高版本,则绝不应将其设为存储迭代器。当然,C++ 标准库并不总是与自身一致。但它通常会指出其不一致之处。
path::iterator
是一个存储迭代器。标准说它是一个双向迭代器;但是,它也为这种类型提供了引用/指针保留规则的例外。这意味着您不能传递path::iterator
给任何可能依赖于该保留规则的代码。
现在,这并不意味着你不能将它传递给任何东西。任何只需要 InputIterator 的算法都可以采用这样的迭代器,因为这样的代码不能依赖于该规则。当然,您编写的任何代码或在其文档中明确指出它不依赖该规则的任何代码都可以使用。但是不能保证你可以使用reverse_iterator
它,即使它说它是一个双向迭代器。
regex_iterator
s在这方面就更差了。根据它们的标签,它们被称为 ForwardIterators,但标准从未说它们实际上是ForwardIterators(与 不同path::iterator
)。并且将它们指定为reference
对成员对象的实际引用使得它们不可能成为真正的 ForwardIterators。
请注意,我没有区分 C++20 之前的概念和 Ranges 概念。这是因为 ForwardIterator 概念仍然禁止存储迭代器。这是设计使然。
用法
现在显然,您可以在代码中做任何您想做的事情。但是您无法控制的代码将在其所有者的域下。他们将针对旧概念、新概念或他们指定的某些其他 c/Concept 或要求进行编写。所以你的迭代器需要能够满足他们的需求。
Ranges 添加带来的算法使用新的概念,因此您始终可以依赖它们来使用代理迭代器。但是,据我了解,范围概念不会向后移植到旧算法中。
就个人而言,我建议完全避免隐藏迭代器实现。通过提供对代理迭代器的完整支持,大多数存储迭代器可以被重写为返回值而不是对对象的引用。
例如,如果有一个path_view
类型,path::iterator
可能会返回它而不是一个完整的path
. 这样,如果你想做昂贵的复制操作,你可以。同样,regex_iterator
s 可能已经返回了匹配对象的副本。新概念通过支持代理迭代器使以这种方式工作成为可能。
现在,存储迭代器以一种有用的方式处理缓存;迭代器可以缓存它们的结果,这样重复*it
使用只会执行一次昂贵的操作。但请记住存储迭代器的问题:返回对其内容的引用。您不需要仅仅为了获得缓存而这样做。您可以将结果缓存在一个optional<T>
(当迭代器在/递减时,您会使其无效)。所以你仍然可以返回一个值。它可能涉及一个额外的副本,但reference
不应该是一个复杂的类型。
当然,所有这些都意味着它auto &val = *it;
不再是合法代码。但是,auto &&val = *it;
将始终有效。这实际上是 Range TS 版本的迭代器的重要组成部分。