繁体   English   中英

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

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

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

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

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

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

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

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

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

  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;由 juanchopanza 指出)如果您进行非限定调用,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

    如果vector<>::iterator只是一个指针,则找不到std::sort

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

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

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

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

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

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


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

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

可能是,但不是。 如果您的实现不完全是狗屎,包装指针的结构将达到相同的速度。

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

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

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

那说,

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

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

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

因为 STL 的设计理念是,您可以编写对迭代器进行迭代的内容,无论该迭代器是否仅等效于指向内存连续数组(如std::arraystd::vector )元素的指针或其他东西像一个链表、一组键、一些在访问时动态生成的东西等。

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

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

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

这后来被修复,并且std::vector需要在连续内存中,但让std::vector<T>::iterator成为指针可能为时已晚。 也许一些代码已经依赖iterator成为一个class/struct

有趣的是,我发现std::vector<T>::iterator是有效的,并生成了一个“空”迭代器(就像一个空指针) 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对元编程具有明显的影响(例如std::vector<T>::iterator::difference_type将无效,是的,应该使用iterator_traits )。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM