【问题标题】:C++ std::vector<>::iterator is not a pointer, why?C++ std::vector<>::iterator 不是指针,为什么?
【发布时间】:2015-12-15 17:25:59
【问题描述】:

只是简单的介绍,用简单的话。 在 C++ 中,迭代器是“事物”,您至少可以在其上编写解引用运算符 *it、增量运算符 ++it,对于更高级的双向迭代器,减量 --it,最后但并非最不重要的是,随机访问迭代器,我们需要运算符索引it[] 以及可能的加法和减法。

C++ 中的此类“事物”是具有相应运算符重载的类型的对象,或简单明了的指针。

std::vector&lt;&gt; 是一个包装连续数组的容器类,因此指针作为迭代器是有意义的。在网上和一些文献中,您可以找到用作指针的vector.begin()

使用指针的基本原理是开销更少,性能更高,特别是如果优化编译器检测到迭代并执行它的事情(向量指令和其他东西)。对于编译器来说,使用迭代器可能更难优化。

知道了这一点,我的问题是,为什么现代 STL 实现,比如说 MSVC++ 2013 或 Mingw 4.7 中的 libstdc++,使用向量迭代器的特殊类?

【问题讨论】:

  • 问题是:为什么不呢?与您的想法相反,使用类而不是指针并不意味着增加开销,使用类还有其他潜在的好处。
  • 一个原因是安全:库有关于取消引用无效迭代器的断言。
  • 事实证明编译器足够聪明,可以找出向量迭代器类只包含一个指针,并据此进行优化。
  • @stgatilov 我认为这是过时的知识。是的,标准库需要积极的内联功能。但是现代编译器提供了这个,然后是一些。自 2007 年以来,编译器已经发展了很多
  • 一般来说,通用代码具有良好的内联和 comdat 折叠功能,更实用。一个优秀的现代编译器必须擅长该任务以利用现代 C++。没有它,C++ 就会瘫痪。但是,存在很好的现代编译器,它们实际上在这一点上比较普遍。他们继续变得更好。

标签: c++ vector iterator stdvector


【解决方案1】:

我认为原因很简单:最初std::vector 不需要在连续的内存块上实现。 所以接口不能只显示一个指针。

来源:https://stackoverflow.com/a/849190/225186

这个问题后来得到修复,std::vector 必须在连续内存中,但将std::vector&lt;T&gt;::iterator 设为指针可能为时已晚。 也许某些代码已经依赖iterator 成为一个class/struct

有趣的是,我发现 std::vector&lt;T&gt;::iterator 的实现是有效的,并生成了一个“空”迭代器(就像一个空指针)it = {};

    std::vector<double>::iterator it = {};
    assert( &*it == nullptr );

另外,std::array&lt;T&gt;::iteratorstd::initializer_list&lt;T&gt;::iterator 在我看到的实现中的指针 T*

在我看来,一个简单的指针 std::vector&lt;T&gt;::iterator 在理论上是完全可以的。 在实践中,作为一个内置对元编程有明显的影响,(例如std::vector&lt;T&gt;::iterator::difference_type 是无效的,是的,应该使用iterator_traits)。

不是原始指针具有(非常)边际优势,即 不允许 可空性 (it == nullptr) 或默认传导性(如果您对此感兴趣)。 (这个论点对于通用编程观点无关紧要。)

同时,专用类迭代器在其他元编程方面的成本很高,因为如果::iterator 是一个指针,则不需要有专门的方法来检测连续内存(参见@987654322 中的contiguous_iterator_tag @) 和向量上的通用代码可以直接转发给遗留的 C 函数。 仅出于这个原因,我认为迭代器不是指针是一个代价高昂的错误。它只是让与 C 代码交互变得困难(因为您需要另一层函数和类型检测来安全地将内容转发到 C)。

话虽如此,我认为我们仍然可以通过允许从迭代器到指针的自动转换以及从指针到 vector::iterators 的显式 (?) 转换来使事情变得更好。

【讨论】:

    【解决方案2】:

    我通过取消引用并立即再次引用迭代器来绕过这个讨厌的障碍。看起来很可笑,但是满足MSVC……

    class Thing {
      . . .
    };
    
    void handleThing(Thing* thing) {
      // do stuff
    }
    
    vector<Thing> vec;
    // put some elements into vec now
    
    for (auto it = vec.begin(); it != vec.end(); ++it)
      // handleThing(it);   // this doesn't work, would have been elegant ..
      handleThing(&*it);    // this DOES work
    

    【讨论】:

    • 这不能回答问题。这不是一个“讨厌的障碍”;这正是您需要做的以确保您实际获得一个指针,因为迭代器不必是一个,并且在其他(实际)答案中已经提供了很好的理由来说明为什么不应该这样做。显然,更好的模式是采用Thing&amp; 并通过*it。另请注意,您的代码在元素类型可能重载operator&amp;(不建议)的一般情况下不起作用。
    • 这是一个合理的解决方案。不过,稍微澄清一下术语:一旦取消引用 (*) 迭代器,您现在指的是对象本身。所以地址 (&) 是一个指向对象的指针,而不是一个迭代器。
    【解决方案3】:

    迭代器的实现是实现定义,只要满足标准的要求。它可能是一个指向vector 的指针,它会起作用。不使用指针有几个原因;

    • 与其他容器的一致性。
    • 调试和错误检查支持
    • 重载解析,基于类的迭代器允许重载将它们与普通指针区分开来

    如果所有迭代器都是指针,那么 map 上的 ++it 不会将其递增到下一个元素,因为内存不需要不连续。在std:::vector 的连续内存之后,大多数标准容器需要“更智能”的指针——因此需要迭代器。

    迭代器的物理要求与逻辑要求非常吻合,即元素之间的移动是对它们进行迭代的明确定义的“惯用语”,而不仅仅是移动到下一个内存位置。

    这是 STL 最初的设计要求和目标之一;容器之间的正交关系,算法和通过迭代器连接两者。

    现在它们是类,您可以添加一整套错误检查和健全性检查来调试代码(然后删除它以获得更优化的发布代码)。


    鉴于基于类的迭代器带来的积极方面,为什么应该或不应该只使用 std::vector 迭代器的指针 - 一致性。 std::vector 的早期实现确实使用了普通指针,您可以将它们用于 vector。一旦您必须为其他迭代器使用类,考虑到它们带来的积极影响,将其应用于 vector 是一个好主意。

    【讨论】:

    • OP 确实明确询问了vector::iterator,而不是map::iterator
    • @Walter。一致性。 std::vector 的早期实现确实使用了普通指针,您可以将它们用于vector。一旦您必须为其他迭代器使用类,考虑到它们带来的积极影响,将其应用于vector 是一个好主意。
    • @Walter:现在知道了吗?使用迭代器的东西几乎适用于每个 STL 容器,而使用指针仅适用于向量/数组,而且速度并不快!
    • @MarcusMüller 你没明白。你说的不是回答问题。问题不是为什么不对迭代器使用指针?,而是当指针满足所有要求时,为什么vector::iterator 的实现方式与指针不同? RandomAccessIterator)
    • @MarcusMüller 不,指针有 iterator_traits。它有效,AFAIK 指针是随机访问迭代器的有效实现(除非在 C++11 中更改)。
    【解决方案4】:

    您完全正确,vector::iterator 可以通过一个简单的指针来实现(请参阅here)——事实上,迭代器的概念是基于指向数组元素的指针的概念。但是对于其他容器,例如maplistdeque,指针根本不起作用。那么为什么不这样做呢?以下是类实现优于原始指针的三个原因。

    1. 将迭代器实现为单独的类型允许附加功能(超出标准要求的功能),例如(在 Quentins 评论之后的编辑中添加)在取消引用时添加断言的可能性迭代器,例如,在调试模式下。

    2. 重载解析如果迭代器是一个指针T*,它可以作为有效参数传递给采用T* 的函数,而这对于迭代器类型是不可能的。因此,使std::vector&lt;&gt;::iterator 成为指针实际上会改变现有代码的行为。例如,考虑

      template<typename It>
      void foo(It begin, It end);
      void foo(const double*a, const double*b, size_t n=0);
      
      std::vector<double> vec;
      foo(vec.begin(), vec.end());    // which foo is called?
      
    3. 依赖于参数的查找(ADL;由 juanchopanza 指出)如果您进行非限定调用,ADL 确保仅当参数是在 @987654331 中定义的类型时才会搜索 namespace std 中的函数@。所以,

      std::vector<double> vec;
      sort(vec.begin(), vec.end());             // calls std::sort
      sort(vec.data(), vec.data()+vec.size());  // fails to compile
      

      如果vector&lt;&gt;::iterator 只是一个指针,则找不到std::sort

    【讨论】:

    • 重载分辨率可能是最大的原因,+1
    • @BenVoigt 另一个问题是 ADL 不适用于普通指针。除非允许实现在 std 命名空间内定义自定义指针类型。
    • @juanchopanza 如果我没记错的话,只有当vector::iterator(而不是某个模板参数)被某个std 函数显式用作参数时,ADL 才应该是相关的。你有这种功能的例子吗?
    • @Walter 任何接受一对迭代器的函数。 std::vector&lt;int&gt; v; sort(v.begin(), v.end()); 不起作用,除非迭代器位于 std 命名空间中。
    • 如果调试配置使用检查迭代器(类类型)而发布配置不使用,则重载解决方案可能会更糟。
    【解决方案5】:

    使用指针的基本原理是开销更少,更高 性能,特别是如果优化编译器检测到迭代 并做它的事情(向量指令和东西)。使用迭代器 编译器可能更难优化。

    可能是,但不是。如果你的实现不是很糟糕,包装指针的结构将达到相同的速度。

    考虑到这一点,很容易看到更好的诊断消息(命名迭代器而不是 T*)、更好的重载解析、ADL 和调试检查等简单的好处使结构明显胜过指针。原始指针没有任何优势。

    【讨论】:

      【解决方案6】:

      使用指针的基本原理是开销更少,更高 性能,特别是如果优化编译器检测到迭代 并做它的事情(矢量指令和东西)。使用迭代器 编译器可能更难优化。

      这是问题核心的误解。一个格式良好的类实现将没有开销,并且性能相同,这一切都是因为编译器可以优化抽象并将迭代器类视为std::vector 情况下的指针。

      也就是说,

      MSVC++ 2013 或 Mingw 4.7 中的 libstdc++,对向量使用特殊类 迭代器

      因为他们认为添加抽象层class iterator 来定义std::vector 上的迭代概念比为此目的使用普通指针更有益。

      抽象具有不同的成本与收益,通常会增加设计复杂性(不一定与性能或开销相关)以换取灵活性、面向未来的验证、隐藏实现细节。上述编译器认为,这种增加的复杂性是为获得抽象带来的好处而付出的适当代价。

      【讨论】:

        【解决方案7】:

        因为 STL 的设计理念是,您可以在迭代器上编写 迭代 的内容,无论该迭代器是否仅等同于指向内存连续数组元素的指针(例如 @987654321 @ 或 std::vector) 或类似链表、一组键、在访问时即时生成的东西等。

        另外,不要上当:在向量情况下,解引用可能(没有调试选项)只是分解为可内联的指针解引用,因此编译后甚至不会有开销!

        【讨论】:

        • 我不明白这是如何回答这个问题的。你可以用指针同样好地完成所有这些 (iterate)。对于许多容器,迭代器不能是普通指针,但std::vector 这实际上是可能的。
        • @MarcusMüller 那仍然没有回答问题。指针可以作为随机访问迭代器正常工作。并且实现可以很好地使用指针,我能想到的唯一会改变的是ADL不起作用,除非实现可以在std命名空间内定义指针类型。但是 AFAIK,没有任何东西说指针不能被使用。
        • @MarcusMüller 好吧,你不是在这里提出这个论点。
        • @juanchopanza:我很确定 无论该迭代器是否仅等效于指针 [...] 或类似链表的东西,[...] 暗示一致性的论点!
        • 我很确定有人可以提出一个很好的论点,即它暗示一个普通指针很好,因为它满足前向迭代器的所有要求。您的第一段没有解释为什么std::vector::iterator 不是最新流行实现的指针。正如已经提到的,它没有回答这个问题。
        猜你喜欢
        • 2020-07-23
        • 2020-10-30
        • 2012-02-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多