代码之家  ›  专栏  ›  技术社区  ›  Serge Ballesta

对于代理容器上的迭代器,什么是“最不坏的实现”?

  •  2
  • Serge Ballesta  · 技术社区  · 6 年前

    上下文

    我试图实现一个类似nd数组的容器。包装底层序列容器并允许将其作为容器(of…)的容器处理的东西: arr[i][j][k] 应该是 _arr[(((i * dim2) + j) * dim3) + k] 是的。

    好吧,在那之前, arr[i] 必须是子数组上的包装类…

    当我试着执行任务时,我突然意识到周围到处都是龙:

    真正的问题是,一旦有了代理容器,任何迭代器都不能满足前向迭代器的以下要求:

    前向迭代器

    6个 如果 a b 都是不可引用的,那么 a == b 如果且仅当 *a *b 绑定到同一对象。

    示例来自标准库本身:

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

      类向量[vector.bool]

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

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

      路径迭代器[fs.path.itr]

      2个 迭代器是一个常量迭代器,满足双向迭代器的所有要求(27.2.6) 除此之外,对于可解引用的迭代器 类型为path::iterator A==B ,没有要求 那个 *一个 *乙 绑定到同一对象。

      cppreference 以下内容:

      注意:std::reverse_迭代器不能与返回成员对象引用的迭代器(所谓的“隐藏迭代器”)一起使用。存放迭代器的一个例子是std::filesystem::path::迭代器。

    问题

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

    所以我的问题是为什么代理迭代器比隐藏迭代器更好,以及它们中的任何一个允许使用什么算法。如果可能的话,我真的很想找一个 参考 这种迭代器的实现

    作为参考,我的代码的当前实现已提交到 Code Review 是的。它包含一个隐藏迭代器(当我试图使用 std::reverse_iterator )

    1 回复  |  直到 6 年前
        1
  •  5
  •   Nicol Bolas    6 年前

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

    但首先,我需要区分c++pre-20的命名需求和为ts范围创建并包含在c++20中的实际语言概念。它们都被称为“概念”,但它们被定义为 不同的 是的。因此,当我谈到concept-with-a-lowercase-c时,我指的是c++20之前的需求。当我谈到concept-with-a-captial-c时,我指的是c++20之类的东西。

    代理迭代器

    代理迭代器是迭代器,其中 reference 不是 value_type& ,但它是另一种类型,其行为类似于对 value_type 是的。在这种情况下, *it 将prvalue返回到此 参考 是的。

    输入者概念不要求 参考 ,但它可以转换为 值类型 是的。但是,forwardIterator概念明确声明“ 参考 是指 T “。

    因此,代理迭代器不能适应forwarditerator概念。但是 可以 还是个输入者。因此,您可以安全地将代理迭代器传递给任何只需要inputiterators的函数。

    所以,问题是 vector<bool> 迭代器不是代理迭代器。这是因为他们承诺实现随机访问迭代器的概念(尽管使用了适当的标记),而实际上他们只是输入和输出。

    c++20中采用的ranges建议(大多)对迭代器概念进行了更改,这些概念允许代理迭代器用于 全部的 迭代器。所以在范围内, vector<bool>::iterator 真正实现了randomAccessIterator的概念。因此,如果有针对ranges概念编写的代码,那么可以使用各种类型的代理迭代器。

    这对于处理计数范围之类的事情非常有用。你可以 参考 值类型 是同一类型的,所以不管用哪种方法处理整数。

    当然,如果你能控制使用迭代器的代码,你可以让它做你想做的任何事情,只要你不违反迭代器的概念。

    隐藏迭代器

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

    隐藏迭代器通常是实现的,因为计算要返回的值非常昂贵。可能会涉及到内存分配(例如 path::iterator )或者它可能涉及一个可能很复杂的操作,应该只做一次(比如 regex_iterator )中。所以你只想在必要的时候做。

    作为一个概念(或概念),转发迭代器的基础之一是这些迭代器的范围代表存在于值之上的范围。 独立地 他们的迭代器。这允许多路径操作,但也使其他操作变得有用。您可以存储对范围内项的引用,然后在其他地方迭代。

    如果需要迭代器作为forwarditerator或更高版本,则应该 从未 使它成为一个隐藏迭代器。当然,c++标准库并不总是与自身保持一致。但它通常会说出它的不一致之处。

    路径::迭代器 是一个隐藏迭代器。该标准表示它是双向运算符;但是,它也为该类型的引用/指针保留规则提供了异常。这意味着你不能通过 路径::迭代器 任何可能依赖于保存规则的代码。

    现在,这并不意味着你不能把它传递给任何人。任何只需要inputiterator的算法都可以使用这样的迭代器,因为这样的代码不能依赖于该规则。当然,可以使用您编写的任何代码,或者在文档中特别声明它不依赖于该规则的任何代码。但不能保证你能用 reverse_iterator 尽管上面说它是双向的。

    正则迭代器 在这方面,S更糟。据说它们是基于其标记的forwarditerator,但标准从未说明它们实际上是 转发迭代器(与 路径::迭代器 )中。它们的规格是 参考 对成员对象的实际引用使它们不可能是真正的forwarditerator。

    注意,我没有区分pre-c++20概念和ranges概念。这是因为forwarditerator概念仍然禁止隐藏迭代器。 This is by design.

    用法

    很明显,你可以在代码中做任何你想做的事情。但你无法控制的代码将归其所有者所有。他们将针对旧的概念、新的概念或他们指定的其他c/概念或需求进行编写。所以迭代器需要能够与它们的需求兼容。

    ranges加法带来的算法使用了新的概念,因此您可以始终依赖它们来使用代理迭代器。然而,据我所知,范围概念是 后移植到旧算法中。

    就我个人而言,我建议不要完全隐藏迭代器实现。通过提供对代理迭代器的完全支持,大多数隐藏迭代器都可以重写以返回 价值观 而不是对对象的引用。

    例如,如果 path_view 类型, 路径::迭代器 本可以把它还给你而不是一个成熟的 path 是的。这样,如果你想做昂贵的复制操作,你可以。同样地, 正则迭代器 s本可以返回match对象的副本。新的概念通过支持代理迭代器使这种方式成为可能。

    现在,隐藏迭代器以一种有用的方式处理缓存;迭代器可以缓存其结果,以便重复 *它 使用只做一次昂贵的操作。但是请记住隐藏迭代器的问题:返回对其内容的引用。你没有 需要 这样做只是为了得到缓存。您可以将结果缓存在 optional<T> (当迭代器处于/递减状态时无效)。所以你仍然可以返回一个值。可能需要一份额外的副本,但是 参考 不应该是复杂类型。

    当然,所有这些都意味着 auto &val = *it; 不再是法律法规了。然而, auto &&val = *it; 总是有用的。这实际上是范围ts版迭代器的一个重要部分。