C++ std::vector<>::迭代器不是指针,为什么?

C++ std::vector<>::iterator is not a pointer, why?

本文关键字:指针 为什么 gt std vector lt C++ 迭代器      更新时间:2023-10-16

只是一个简单的介绍。在c++中,迭代器是一种"东西",你至少可以在上面写解引用操作符*it,自增操作符++it,对于更高级的双向迭代器,可以写自减操作符--it,最后但并非最不重要的是,对于随机访问迭代器,我们需要操作符索引it[],可能还需要加法和减法。

c++中这样的"东西"是具有根据操作符重载类型的对象,或普通指针和简单指针。

std::vector<>是包装连续数组的容器类,因此指针作为迭代器是有意义的。在网上,在一些文献中,你可以发现vector.begin()被用作指针。

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

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

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

  1. 将迭代器实现为单独的类型允许额外的功能(超出标准所要求的),例如(在quentin注释之后的编辑中添加)在解引用迭代器时添加断言的可能性,例如,在调试模式下。

  2. 重载解析如果迭代器是指针T*,则可以将其作为有效参数传递给以T*为参数的函数,而对于迭代器类型则不能这样做。因此,使std::vector<>::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;如果进行非限定调用,ADL确保只有当参数是namespace std中定义的类型时,才会搜索namespace std中的函数。

    std::vector<double> vec;
    sort(vec.begin(), vec.end());             // calls std::sort
    sort(vec.data(), vec.data()+vec.size());  // fails to compile
    
  4. 如果vector<>::iterator仅仅是一个指针,则无法找到std::sort

迭代器的实现是实现定义的,只要满足标准的要求。它可以是vector的指针,这将工作。不使用指针有以下几个原因:

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

如果所有迭代器都是指针,则map上的++it不会将其自增到下一个元素,因为不要求内存是非连续的。超过std:::vector的连续内存,大多数标准容器需要"更智能"的指针——因此需要迭代器。

迭代器的物理要求与逻辑要求非常吻合,即元素之间的移动必须有一个定义良好的"习惯用法",即遍历元素,而不仅仅是移动到下一个内存位置。

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

既然它们是类,你可以添加一大堆错误检查和健全检查来调试代码(然后删除它以获得更优化的发布代码)。


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

使用指针的基本原理是更少的开销,更高性能,特别是当优化编译器检测到迭代时然后做它的事情(矢量指令之类的)。使用迭代器可能会使编译器更难优化。

它可能是,但它不是。如果你的实现不是完全正确的,一个包含指针的结构体将达到相同的速度。

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

使用指针的基本原理是更少的开销,更高性能,特别是当优化编译器检测到迭代时然后做它的事情(矢量指令之类的)。使用迭代器可能会使编译器更难优化。

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

话虽如此,

msvc++ 2013或libstdc++在mingw4.7,使用一个特殊的类向量迭代器

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

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

因为STL的设计思想是,你可以写一些在一个迭代器上迭代的东西,不管这个迭代器只是相当于一个指向内存连续数组(如std::arraystd::vector)元素的指针,还是像链表、键集这样的东西,在访问时动态生成的东西,等等

同样,不要被愚弄了:在vector的情况下,解引用可能(没有调试选项)会分解成一个内联指针解引用,所以编译后甚至不会有开销!

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

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

这个问题后来被修复了,std::vector被要求在连续内存中,但是把std::vector<T>::iterator变成一个指针可能太晚了。也许有些代码已经依赖于iterator class/struct

有趣的是,我发现std::vector<T>::iterator的实现中这是有效的,并生成了一个"null"迭代器(就像空指针)it = {}; .

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

此外,std::array<T>::iteratorstd::initializer_list<T>::iterator 指针T*在我看到的实现。

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

不是原始指针具有不允许为空性(it == nullptr)或默认导电性(如果您喜欢)的(非常)边际优势。(从泛型编程的角度来看,这是一个无关紧要的参数。)

同时,专用类迭代器在其他元编程方面的成本很高,因为如果::iterator是一个指针,就不需要有专门的方法来检测连续内存(参见https://en.cppreference.com/w/cpp/iterator/iterator_tags中的contiguous_iterator_tag),并且向量上的泛型代码可以直接转发给遗留的c函数。仅凭这个原因,我就认为迭代器不是指针是一个代价高昂的错误。它只是使与C代码交互变得困难(因为你需要另一层函数和类型检测来安全地将内容转发给C)。

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

我通过解引用并立即再次引用迭代器来绕过这个讨厌的障碍。这看起来很荒谬,但它满足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