代理容器上的迭代器的 "least bad implementation" 可能是什么?
What could be a "least bad implementation" for an iterator over a proxied container?
上下文
我正在尝试实现一个类似容器的 nD 数组。可以包装底层序列容器并允许将其作为容器容器处理的东西(...):arr[i][j][k]
应该是 _arr[(((i * dim2) + j) * dim3) + k]
.
的(最终为 const)引用
好的,直到那里,arr[i]
只是作为子数组的包装器 class...
而当我尝试实现交互器时,我突然发现周围到处都是龙:
- 我的容器不是标准兼容容器,因为
operator []
returns 代理或包装器而不是真正的引用 (When Is a Container Not a Container?)
- 这会导致迭代器成为 stashing 迭代器(已知是错误的 (Reference invalidation after applying reverse_iterator on a custom made iterator and its accepted answer)
- ... 或代理迭代器不一定更好 (To Be or Not to Be (an Iterator))
真正的问题是一旦有了代理容器,迭代器就无法满足以下对前向迭代器的要求:
Forward iterators [forward.iterators]
...
6 If a
and b
are both dereferenceable, then a == b
if and only if *a
and *b
are bound to the same object.
示例来自标准库本身:
众所周知,vector<bool>
不遵守容器的所有要求,因为它 returns 代理而不是引用:
Class vector [vector.bool]
...
3 There is no requirement that the data be stored as a contiguous allocation of bool values. A space-optimized
representation of bits is recommended instead.
4 reference is a class that simulates the behavior of references of a single bit in vector.
已知文件系统路径迭代器是一个隐藏迭代器:
path iterators [fs.path.itr]
...
2 A path::iterator is a constant iterator satisfying all the requirements of a bidirectional iterator (27.2.6)
except that, for dereferenceable iterators a
and b
of type path::iterator with a == b
, there is no requirement
that*a
and *b
are bound to the same object.
来自cppreference:
Notes: std::reverse_iterator does not work with iterators that return a reference to a member object (so-called "stashing iterators"). An example of stashing iterator is std::filesystem::path::iterator.
问题
我目前发现了很多关于为什么代理容器不是真正的容器以及为什么如果标准允许代理容器和迭代器会很好的参考资料。但我仍然不明白什么是最好的,什么是真正的限制。
所以我的问题是为什么代理迭代器确实比存储迭代器更好,以及它们中的任何一个都允许使用什么算法。如果可能的话,我真的很想为这样的迭代器
找到一个reference实现
作为参考,我的代码的当前实现已在 Code Review 上提交。它包含一个隐藏迭代器(当我尝试使用 std::reverse_iterator
时它立即崩溃)
好的,我们有两个相似但不同的概念。所以让我们把它们摆出来。
但首先,我需要区分 C++-pre-20 的命名要求与为 Ranges TS 创建并包含在 C++20 中的实际语言内概念。它们都被称为“概念”,但它们的定义不同。因此,当我谈到 concept-with-a-lowercase-c 时,我指的是 C++20 之前的要求。当我谈论 Concept-with-a-captial-C 时,我指的是 C++20 的东西。
代理迭代器
代理迭代器是这样一种迭代器,其中它们的 reference
不是 value_type&
,而是一些其他类型,其行为类似于对 value_type
的引用。在这种情况下,*it
return 是此 reference
.
的纯右值
InputIterator 概念对 reference
没有任何要求,除了它可以转换为 value_type
。但是,ForwardIterator 概念明确声明“reference
是对 T
”的引用。
因此,代理迭代器不适合 ForwardIterator 概念。但它可以仍然是一个 InputIterator。因此,您可以安全地将代理迭代器传递给任何只需要 InputIterators 的函数。
因此,vector<bool>
s 迭代器的问题不在于它们是代理迭代器。他们承诺他们实现了 RandomAccessIterator 概念(尽管使用了适当的标签),而实际上他们只是 InputIterators 和 OutputIterators。
C++20 中采用的范围提案(大部分)对迭代器概念进行了更改,允许代理迭代器用于 所有 迭代器。所以在 Ranges 下,vector<bool>::iterator
真正实现了 RandomAccessIterator 的概念。因此,如果您有针对范围概念编写的代码,那么您可以使用各种代理迭代器。
这对于处理计数范围之类的事情非常有用。你可以让 reference
和 value_type
是相同的类型,所以你只是处理整数。
当然,如果您可以控制使用迭代器的代码,您可以让它做任何您想做的事情,只要您不违反编写迭代器所针对的概念。
隐藏迭代器
存储迭代器是迭代器,其中 reference_type
是(直接或间接)对存储在迭代器中的对象的引用。因此,如果您制作迭代器的副本,副本将 return 引用与原始对象不同的对象,即使它们引用相同的元素。当您递增迭代器时,以前的引用不再有效。
通常会实现存储迭代器,因为计算您想要 return 的值非常昂贵。也许它会涉及内存分配(例如 path::iterator
),或者它可能会涉及一个可能只应该执行一次的复杂操作(例如 regex_iterator
)。所以你只想在必要的时候做。
ForwardIterator 作为一个概念(或概念)的基础之一是这些迭代器的范围表示值的范围,这些值独立 存在于它们的迭代器中。这允许多通道操作,但它也使做其他事情变得有用。您可以存储对该范围内项目的引用,然后在其他地方进行迭代。
如果您需要一个迭代器成为 ForwardIterator 或更高版本,您应该永远不要使它成为一个隐藏迭代器。当然,C++标准库并不总是与自身保持一致。但它通常会指出其不一致之处。
path::iterator
是一个存储迭代器。标准说它是一个双向迭代器;但是,它也为这种类型提供了 reference/pointer 保留规则的例外。这意味着您不能将 path::iterator
传递给可能依赖于该保留规则的任何代码。
现在,这并不意味着您不能将它传递给任何东西。任何只需要 InputIterator 的算法都可以采用这样的迭代器,因为这样的代码不能依赖该规则。当然,您编写的任何代码或在其文档中明确声明它不依赖于该规则的任何代码都可以使用。但是不能保证你可以在它上面使用 reverse_iterator
,即使它说它是一个 BidirectionalIterator.
regex_iterator
在这方面更差。根据它们的标签,它们被称为 ForwardIterators,但标准从未说它们实际上 是 ForwardIterators(与 path::iterator
不同)。并且将它们指定为 reference
是对成员对象的实际引用,这使得它们不可能成为真正的 ForwardIterators。
请注意,我没有区分 C++20 之前的概念和范围概念。那是因为 ForwardIterator 概念仍然禁止隐藏迭代器。 This is by design.
用法
很明显,您可以在代码中做任何您想做的事情。但是您无法控制的代码将在其所有者的域中。他们将针对旧概念、新概念或他们指定的其他一些 c/Concept 或要求进行写作。所以你的迭代器需要能够兼容他们的需求。
Ranges 添加带来的算法使用了新的概念,因此您始终可以依靠它们来使用代理迭代器。但是,据我了解,范围概念未向后移植到旧算法中。
就个人而言,我建议避免完全隐藏迭代器实现。通过为代理迭代器提供完整的支持,大多数隐藏迭代器可以重写为 return values 而不是对对象的引用。
例如,如果有 path_view
类型,path::iterator
可以 return 编辑它而不是完整的 path
。这样,如果你想做昂贵的复制操作,你可以。类似地,regex_iterator
s 可以有匹配对象的 returned 副本。新概念通过支持代理迭代器使这种工作方式成为可能。
现在,存储迭代器以一种有用的方式处理缓存;迭代器可以缓存它们的结果,这样重复 *it
的使用只会执行一次昂贵的操作。但是请记住隐藏迭代器的问题:return引用它们的内容。您 不需要 这样做只是为了获得缓存。您可以将结果缓存在 optional<T>
中(当迭代器为 in/decremented 时,它会失效)。所以你仍然可以return一个值。它可能涉及额外的副本,但 reference
不应该是复杂类型。
当然,所有这些都意味着 auto &val = *it;
不再是合法代码。但是,auto &&val = *it;
将始终有效。这实际上是迭代器 Range TS 版本的很大一部分。
上下文
我正在尝试实现一个类似容器的 nD 数组。可以包装底层序列容器并允许将其作为容器容器处理的东西(...):arr[i][j][k]
应该是 _arr[(((i * dim2) + j) * dim3) + k]
.
好的,直到那里,arr[i]
只是作为子数组的包装器 class...
而当我尝试实现交互器时,我突然发现周围到处都是龙:
- 我的容器不是标准兼容容器,因为
operator []
returns 代理或包装器而不是真正的引用 (When Is a Container Not a Container?) - 这会导致迭代器成为 stashing 迭代器(已知是错误的 (Reference invalidation after applying reverse_iterator on a custom made iterator and its accepted answer)
- ... 或代理迭代器不一定更好 (To Be or Not to Be (an Iterator))
真正的问题是一旦有了代理容器,迭代器就无法满足以下对前向迭代器的要求:
Forward iterators [forward.iterators]
...
6 Ifa
andb
are both dereferenceable, thena == b
if and only if*a
and*b
are bound to the same object.
示例来自标准库本身:
-
众所周知,
vector<bool>
不遵守容器的所有要求,因为它 returns 代理而不是引用:Class vector [vector.bool]
...
3 There is no requirement that the data be stored as a contiguous allocation of bool values. A space-optimized representation of bits is recommended instead.
4 reference is a class that simulates the behavior of references of a single bit in vector.已知文件系统路径迭代器是一个隐藏迭代器:
path iterators [fs.path.itr]
...
2 A path::iterator is a constant iterator satisfying all the requirements of a bidirectional iterator (27.2.6) except that, for dereferenceable iteratorsa
andb
of type path::iterator witha == b
, there is no requirement that*a
and*b
are bound to the same object.来自cppreference:
Notes: std::reverse_iterator does not work with iterators that return a reference to a member object (so-called "stashing iterators"). An example of stashing iterator is std::filesystem::path::iterator.
问题
我目前发现了很多关于为什么代理容器不是真正的容器以及为什么如果标准允许代理容器和迭代器会很好的参考资料。但我仍然不明白什么是最好的,什么是真正的限制。
所以我的问题是为什么代理迭代器确实比存储迭代器更好,以及它们中的任何一个都允许使用什么算法。如果可能的话,我真的很想为这样的迭代器
找到一个reference实现作为参考,我的代码的当前实现已在 Code Review 上提交。它包含一个隐藏迭代器(当我尝试使用 std::reverse_iterator
时它立即崩溃)
好的,我们有两个相似但不同的概念。所以让我们把它们摆出来。
但首先,我需要区分 C++-pre-20 的命名要求与为 Ranges TS 创建并包含在 C++20 中的实际语言内概念。它们都被称为“概念”,但它们的定义不同。因此,当我谈到 concept-with-a-lowercase-c 时,我指的是 C++20 之前的要求。当我谈论 Concept-with-a-captial-C 时,我指的是 C++20 的东西。
代理迭代器
代理迭代器是这样一种迭代器,其中它们的 reference
不是 value_type&
,而是一些其他类型,其行为类似于对 value_type
的引用。在这种情况下,*it
return 是此 reference
.
InputIterator 概念对 reference
没有任何要求,除了它可以转换为 value_type
。但是,ForwardIterator 概念明确声明“reference
是对 T
”的引用。
因此,代理迭代器不适合 ForwardIterator 概念。但它可以仍然是一个 InputIterator。因此,您可以安全地将代理迭代器传递给任何只需要 InputIterators 的函数。
因此,vector<bool>
s 迭代器的问题不在于它们是代理迭代器。他们承诺他们实现了 RandomAccessIterator 概念(尽管使用了适当的标签),而实际上他们只是 InputIterators 和 OutputIterators。
C++20 中采用的范围提案(大部分)对迭代器概念进行了更改,允许代理迭代器用于 所有 迭代器。所以在 Ranges 下,vector<bool>::iterator
真正实现了 RandomAccessIterator 的概念。因此,如果您有针对范围概念编写的代码,那么您可以使用各种代理迭代器。
这对于处理计数范围之类的事情非常有用。你可以让 reference
和 value_type
是相同的类型,所以你只是处理整数。
当然,如果您可以控制使用迭代器的代码,您可以让它做任何您想做的事情,只要您不违反编写迭代器所针对的概念。
隐藏迭代器
存储迭代器是迭代器,其中 reference_type
是(直接或间接)对存储在迭代器中的对象的引用。因此,如果您制作迭代器的副本,副本将 return 引用与原始对象不同的对象,即使它们引用相同的元素。当您递增迭代器时,以前的引用不再有效。
通常会实现存储迭代器,因为计算您想要 return 的值非常昂贵。也许它会涉及内存分配(例如 path::iterator
),或者它可能会涉及一个可能只应该执行一次的复杂操作(例如 regex_iterator
)。所以你只想在必要的时候做。
ForwardIterator 作为一个概念(或概念)的基础之一是这些迭代器的范围表示值的范围,这些值独立 存在于它们的迭代器中。这允许多通道操作,但它也使做其他事情变得有用。您可以存储对该范围内项目的引用,然后在其他地方进行迭代。
如果您需要一个迭代器成为 ForwardIterator 或更高版本,您应该永远不要使它成为一个隐藏迭代器。当然,C++标准库并不总是与自身保持一致。但它通常会指出其不一致之处。
path::iterator
是一个存储迭代器。标准说它是一个双向迭代器;但是,它也为这种类型提供了 reference/pointer 保留规则的例外。这意味着您不能将 path::iterator
传递给可能依赖于该保留规则的任何代码。
现在,这并不意味着您不能将它传递给任何东西。任何只需要 InputIterator 的算法都可以采用这样的迭代器,因为这样的代码不能依赖该规则。当然,您编写的任何代码或在其文档中明确声明它不依赖于该规则的任何代码都可以使用。但是不能保证你可以在它上面使用 reverse_iterator
,即使它说它是一个 BidirectionalIterator.
regex_iterator
在这方面更差。根据它们的标签,它们被称为 ForwardIterators,但标准从未说它们实际上 是 ForwardIterators(与 path::iterator
不同)。并且将它们指定为 reference
是对成员对象的实际引用,这使得它们不可能成为真正的 ForwardIterators。
请注意,我没有区分 C++20 之前的概念和范围概念。那是因为 ForwardIterator 概念仍然禁止隐藏迭代器。 This is by design.
用法
很明显,您可以在代码中做任何您想做的事情。但是您无法控制的代码将在其所有者的域中。他们将针对旧概念、新概念或他们指定的其他一些 c/Concept 或要求进行写作。所以你的迭代器需要能够兼容他们的需求。
Ranges 添加带来的算法使用了新的概念,因此您始终可以依靠它们来使用代理迭代器。但是,据我了解,范围概念未向后移植到旧算法中。
就个人而言,我建议避免完全隐藏迭代器实现。通过为代理迭代器提供完整的支持,大多数隐藏迭代器可以重写为 return values 而不是对对象的引用。
例如,如果有 path_view
类型,path::iterator
可以 return 编辑它而不是完整的 path
。这样,如果你想做昂贵的复制操作,你可以。类似地,regex_iterator
s 可以有匹配对象的 returned 副本。新概念通过支持代理迭代器使这种工作方式成为可能。
现在,存储迭代器以一种有用的方式处理缓存;迭代器可以缓存它们的结果,这样重复 *it
的使用只会执行一次昂贵的操作。但是请记住隐藏迭代器的问题:return引用它们的内容。您 不需要 这样做只是为了获得缓存。您可以将结果缓存在 optional<T>
中(当迭代器为 in/decremented 时,它会失效)。所以你仍然可以return一个值。它可能涉及额外的副本,但 reference
不应该是复杂类型。
当然,所有这些都意味着 auto &val = *it;
不再是合法代码。但是,auto &&val = *it;
将始终有效。这实际上是迭代器 Range TS 版本的很大一部分。