【问题标题】:What could be a "least bad implementation" for an iterator over a proxied container?对于代理容器上的迭代器来说,什么是“最糟糕的实现”?
【发布时间】:2018-12-05 09:53:03
【问题描述】:

上下文

我试图实现一个像容器一样的 nD 数组。可以包装底层序列容器并允许将其作为容器的容器(...)处理的东西:arr[i][j][k] 应该是_arr[(((i * dim2) + j) * dim3) + k] 的(最终是 const)引用。

到此为止,arr[i] 只是子数组的包装类...

当我尝试实现交互器时,我突然意识到龙无处不在:

真正的问题是,一旦你有一个代理容器,没有迭代器可以遵守以下对前向迭代器的要求:

转发迭代器 [forward.iterators]
...
6 如果ab 都是可解引用的,那么a == b 当且仅当*a*b 绑定到同一个对象。 p>

示例来自标准库本身:

  • vector<bool> 不尊重容器的所有要求,因为它返回的是代理而不是引用:

    类向量 [vector.bool]
    ...
    3 不要求将数据存储为 bool 值的连续分配。空间优化 建议使用位表示。
    4reference 是一个模拟向量中单个位的引用行为的类。

  • 文件系统路径迭代器已知是一个隐藏迭代器:

    路径迭代器 [fs.path.itr]
    ...
    2 path::iterator 是一个常量迭代器,满足双向迭代器的所有要求 (27.2.6 ) 除了,对于带有a == b 的path::iterator 类型的可解引用迭代器ab,没有要求 那*a*b 绑定到同一个对象。

    来自cppreference

    注意:std::reverse_iterator 不适用于返回对成员对象的引用的迭代器(所谓的“存储迭代器”)。存储迭代器的一个例子是 std::filesystem::path::iterator。

问题

我目前找到了大量关于为什么代理容器不是真正的容器以及如果标准允许代理容器和迭代器会很好的参考资料。但我仍然不明白什么是最好的,什么是真正的限制。

所以我的问题是为什么代理迭代器真的比存储迭代器更好,以及它们中的任何一个都允许使用哪些算法。如果可能的话,我真的很想为这样的迭代器找到一个 reference 实现

作为参考,我的代码的当前实现已在Code Review 上提交。它包含一个存储迭代器(当我尝试使用 std::reverse_iterator 时立即中断)

【问题讨论】:

  • 我知道这是一个 give me ze code 问题,但至少有一些研究,我认为这样的示例实现在 SO 上会很好。
  • 请忽略返回引用的前向迭代器要求,并以非常非正式的方式阅读“绑定到同一个对象”。
  • 实现什么目的的“最不糟糕的实施”?与任何概念一样,重要的是使用它的代码的期望。只要每个使用它的人知道它是一个代理迭代器并相应地对待它,代理迭代器就没有任何问题。 “存储”迭代器也是如此。这些问题是当您想将它们传递给具有代理/存储迭代器无法满足的期望的算法(例如在标准库中)时。
  • @MarcGlisse:“请忽略返回引用的前向迭代器要求,并以非常非正式的方式阅读“绑定到同一个对象”。“你不能忽略它。它是标准的一部分,接受 ForwardIterators 编写的代码有权执行value_type_t<Iterator> &val = *it;,其中value_type_t 从迭代器中获取值类型。
  • @SergeBallesta:“但我敢打赌,有些人可能会工作......”这有什么好处?你不知道哪个会起作用。你不知道明天哪个会停止工作。你不知道它是否适用于 libc++ 或 libstdc++ 或 msvc 的标准库。请记住:像auto &val = *it; 这样无害的东西会破坏代理迭代器。那么在没有保证的情况下“工作”有什么好处呢?

标签: c++ iterator proxy-pattern


【解决方案1】:

好的,我们有两个相似但不同的概念。所以让我们把它们布置出来。

但首先,我需要区分 C++-pre-20 的命名要求和为 Ranges TS 创建并包含在 C++20 中的实际语言概念。它们都被称为“概念”,但它们的定义不同。因此,当我谈论带有小写字母 c 的概念时,我指的是 C++20 之前的要求。当我谈论 Concept-with-a-captial-C 时,我指的是 C++20 的东西。

代理迭代器

代理迭代器是指其reference 不是value_type&,而是一些其他类型的迭代器,其行为类似于对value_type 的引用。在这种情况下,*it 会为此 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 概念编写的代码,那么您可以使用各种代理迭代器。

这对于处理计数范围之类的事情非常有用。你可以让referencevalue_type 是同一类型,所以你只是在处理整数。

当然,如果您可以控制使用迭代器的代码,则可以让它做任何您想做的事情,只要您不违反编写迭代器所针对的概念。

存储迭代器

Stashing 迭代器是其中reference_type 是(直接或间接)对存储在迭代器中的对象的引用的迭代器。因此,如果您制作一个迭代器的副本,则该副本将返回对与原始对象不同的对象的引用,即使它们引用相同的元素。当你增加迭代器时,以前的引用不再有效。

通常会实现存储迭代器,因为计算要返回的值很昂贵。也许它会涉及内存分配(例如path::iterator),或者它可能涉及一个可能很复杂的操作,应该只执行一次(例如regex_iterator)。所以你只想在必要时这样做。

作为一个概念(或概念)的 ForwardIterator 的基础之一是这些迭代器的范围代表了一个值范围,这些值独立存在于它们的迭代器中。这允许多通道操作,但它也使做其他事情变得有用。您可以存储对范围内项目的引用,然后在其他地方进行迭代。

如果您需要将迭代器设为 ForwardIterator 或更高版本,则应该从不将其设为存储迭代器。当然,C++ 标准库并不总是与自身一致。但它通常会指出其不一致之处。

path::iterator 是一个存储迭代器。标准说它是一个双向迭代器;但是,它也为这种类型提供了引用/指针保留规则的例外。这意味着您不能将path::iterator 传递给任何可能依赖于该保留规则的代码。

现在,这并不意味着您不能将其传递给任何东西。任何只需要 InputIterator 的算法都可以采用这样的迭代器,因为这样的代码不能依赖于该规则。当然,您编写的任何代码或在其文档中明确指出它不依赖该规则的任何代码都可以使用。但是不能保证你可以在上面使用reverse_iterator,即使它说它是一个双向迭代器。

regex_iterators 在这方面更差。根据它们的标签,它们被称为 ForwardIterators,但标准从未说它们实际上是 ForwardIterators(不像path::iterator)。并且将它们指定为使 reference 成为对成员对象的实际引用使得它们不可能成为真正的 ForwardIterators。

请注意,我没有区分 C++20 之前的概念和 Ranges 概念。这是因为 ForwardIterator 概念仍然禁止存储迭代器。 This is by design.

用法

现在显然,您可以在代码中做任何您想做的事情。但是您无法控制的代码将在其所有者的域下。他们将针对旧概念、新概念或他们指定的某些其他 c/Concept 或要求进行编写。因此,您的迭代器需要能够满足他们的需求。

Ranges 添加带来的算法使用新概念,因此您始终可以依赖它们来使用代理迭代器。但是,据我了解,范围概念没有向后移植到旧算法中。

就个人而言,我建议避免完全隐藏迭代器实现。通过提供对代理迭代器的完整支持,大多数存储迭代器可以重写为返回,而不是对对象的引用。

例如,如果有 path_view 类型,path::iterator 可能会返回该类型,而不是完整的 path。这样,如果你想做昂贵的复制操作,你可以。同样,regex_iterators 可能已返回匹配对象的副本。新概念通过支持代理迭代器使这种工作方式成为可能。

现在,存储迭代器以一种有用的方式处理缓存;迭代器可以缓存它们的结果,这样重复使用*it 只会执行一次昂贵的操作。但请记住存储迭代器的问题:返回对其内容的引用。您需要只是为了获得缓存而这样做。您可以将结果缓存在 optional<T> 中(当迭代器在/递减时,您会使其无效)。所以你仍然可以返回一个值。它可能涉及额外的副本,但reference 不应该是复杂类型。

当然,所有这些都意味着auto &val = *it; 不再是合法代码。但是,auto &&val = *it; 将始终有效。这实际上是 Range TS 版本的迭代器的重要组成部分。

【讨论】:

  • 非常好。只是一个挑剔:即使一个算法只需要一个,比如说 ForwardIterator,如果它得到一个 RandomAccessIterator,它可能(并且确实应该)根据附加功能采取捷径。所以作为一个真正的 ForwardIterator 但一个假的 RandomAccessIterator 可能会使一个算法只需要一个 ForwardIterator 无论如何都会绊倒。士气:说谎是危险的,即使你并不总是被抓住。
  • 感谢您提供如此详细的回答。我没有立即接受它,因为我希望有一个实现示例,但要求更多是不公平的。
猜你喜欢
  • 2023-02-03
  • 2023-04-10
  • 2012-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-12
  • 2018-03-14
  • 2016-03-30
相关资源
最近更新 更多