【问题标题】:C++ STL: Which method of iteration over a STL container is better?C++ STL:哪种迭代 STL 容器的方法更好?
【发布时间】:2010-10-17 12:51:34
【问题描述】:

这对你们中的一些人来说可能看起来很无聊,但是以下两种对 STL 容器进行迭代的方法中哪一种更好? 为什么

class Elem;
typedef vector<Elem> ElemVec;
ElemVec elemVec;

// Method 0
for (ElemVec::iterator i = elemVec.begin(); i != elemVec.end(); ++i)
{
    Elem& e = *i;
    // Do something
}

// Method 1
for (int i = 0; i < elemVec.size(); ++i)
{
    Elem& e = elemVec.at(i);
    // Do something
}

方法 0 看起来像更简洁的 STL,但方法 1 用更少的代码实现了相同的效果。对容器的简单迭代是all 出现在任何源代码中的位置。所以,我倾向于选择方法 1,它似乎可以减少视觉混乱和代码大小。

PS:我知道迭代器可以做的不仅仅是一个简单的索引。但是,请保持回复/讨论的重点是对容器的简单迭代,如上所示。

【问题讨论】:

    标签: c++ stl iterator containers


    【解决方案1】:

    第一个版本适用于任何容器,因此在将任何容器作为参数的模板函数中更有用。可以想象,它的效率也会稍高一些,即使对于向量也是如此。

    第二个版本仅适用于向量和其他整数索引容器。对于那些容器来说,它会更惯用一些,对于 C++ 的新手来说很容易理解,并且如果您需要对索引执行其他操作,这很有用,这并不罕见。

    像往常一样,恐怕没有简单的“这个更好”的答案。

    【讨论】:

    • 谢谢尼尔。我的代码通常不使用模板,而是使用已知类型的向量。您能否详细说明为什么方法 0 在您的答案中更有效?
    • 如果迭代器实际上直接实现为指针,效率可能会稍微高一些。请注意“可能”和“稍微”这两个词的使用——您需要测量以确保。
    • 是的,实际上迭代器对于大多数容器来说只不过是一个指针。但是,这如何使代码更快? AFAIK 甚至方法 1 最终都是指针算术,不是吗?
    • @ash 是的,但是 (ptr+index) 和 (index++) 都要做算术,更不用说 [] 如果没有被内联,它可能是一个函数调用。但就像我说的 - 你需要衡量。
    • 郑重声明,我从未见过性能上有明显差异。
    【解决方案2】:

    如果您不介意(非常?)小的效率损失,我建议您使用Boost.Foreach

    BOOST_FOREACH( Elem& e, elemVec )
    {
        // Your code
    }
    

    【讨论】:

    • 我现在基本上是个 Boost 文盲。感谢您的提示 Benoît!这是一个守门员:-)
    • +1,Benoît,我到处都有循环,在使用真正支持 foreach 的其他语言后,BOOST_FOREACH 让我保持清醒
    • C++ 也有真正的 foreach 支持:std::for_each。语法有点不同;)
    • Jalf:STL 有 for_each,但这几乎不是大多数循环使用的方式,因为它需要定义另一个函数。
    • 当 lambda 自带 C++09 时 stl::foreach 会很有用
    【解决方案3】:

    方法 0 更快,因此推荐。

    方法 1 使用 size(),允许为 O(1),具体取决于容器和 stl 实现。

    【讨论】:

    • 谢谢 Stefan,我忘了 size() :-)
    • size() 在标准兼容向量中应该是 O(1)。而且它的效率不低于 v.end() 因为它可能会被内联。这里的效率是一样的(每次访问不超过几条指令差异)
    • @DavidRodríguez-dribeas:效率通常不一样,因为第一种方法需要额外的指针加载(即在添加偏移量之前加载指向数据的指针)。如果您还有其他代码,编译器可能会对别名感到困惑并避免缓存该指针,从而使您从内存中发出两倍于您需要的负载。微不足道的循环不太可能发生,但在实践中不会出现。
    • @Mehrdad:size() 的缓存可能不是初始代码的问题(评论仅针对size() 的缓存)。在OP中,对vector的访问是通过at(),这涉及到代码中的一个额外的分支,以及一些额外的代码。
    • @DavidRodríguez-dribeas:我说的是指针的缓存size() 不是指针。我说的是begin()end()——使用迭代器/指针通常比索引更快,因为它需要更少的负载。 (at() 显然速度较慢,但​​我说的是常规的、未经检查的索引。)
    【解决方案4】:

    以下迭代标准库容器的方法是最好的。

    使用(及以后)的range-based for-loopauto specifier

    // Method 2
    for (auto& e: elemVec)
    {
        // Do something with e...
    }
    

    这类似于您的Method 0,但需要更少的输入和维护,并且适用于与std::begin()std::end() 兼容的任何容器,包括 普通数组。

    【讨论】:

    • -1, auto& 对这个 Q 是正确的等价物,这个代码也是错误的,我不是一个迭代器
    • @NoSenseEtAl:感谢您帮助我改进答案☺
    【解决方案5】:

    方法0的更多优点:

    • 如果您从矢量移动到另一个 容器循环保持不变,
    • 易于从迭代器移动到 const_iterator 如果你需要,
    • 当 c++0x 到达时,自动 打字会减少一些代码混乱。

    主要缺点是在许多情况下您扫描两个容器,在这种情况下,索引比保留两个迭代器更干净。

    【讨论】:

    • 谢谢大卫。我猜你的意思是方法 0 ;-)
    • 是的,大卫,您能否编辑您的答案以反映这一点?谢谢:-)
    【解决方案6】:

    方法 0,有几个原因。

    • 它更好地表达了您的意图,有助于编译器优化和可读性
    • 一个错误的可能性较小
    • 即使将向量替换为没有 operator[] 的列表,它也可以工作

    当然,最好的解决方案通常是解决方案 2:标准算法之一。 std::for_each、std::transform、std::copy 或您需要的任何其他内容。这还有一些其他优势:

    • 它更好地表达了您的意图,并允许一些重要的编译器优化(MS 的安全 SCL 对您的方法 0 和 1 执行边界检查,但会在标准算法上跳过它)
    • 它的代码更少(至少在调用站点。当然你必须编写一个仿函数或其他东西来替换循环体,但是在使用站点,代码被清理了很多,这可能是最重要。

    一般来说,避免过度指定您的代码。准确指定您想要完成的操作,仅此而已。 std 算法通常是去那里的方法,但即使没有它们,如果你不需要索引i,为什么要有它?在这种情况下,请改用迭代器。

    【讨论】:

      【解决方案7】:

      巧合的是,我最近做了一个速度测试(用 rand() 填充 10 * 1024 * 1024 个整数)。
      这些是 3 次运行,时间以纳秒为单位

      vect[i] time      : 373611869  
      vec.at(i) time    : 473297793  
      *it = time        : 446818590  
      arr[i] time       : 390357294  
      *ptr time         : 356895778  
      

      更新:添加了 stl 算法 std::generate,由于特殊的迭代器优化 (VC++2008),它似乎运行得最快。时间以微秒为单位。

      vect[i] time      : 393951
      vec.at(i) time    : 551387
      *it = time        : 596080
      generate = time   : 346591
      arr[i] time       : 375432
      *ptr time         : 334612
      

      结论:使用标准算法,它们可能比显式循环更快! (也是很好的做法)

      更新:上述时间处于 I/O-bound 情况,我对 CPU-bound 进行了相同的测试(迭代一个相对较短的向量,它应该重复适合缓存,将每个元素乘以 2 并写入返回矢量)

      //Visual Studio 2008 Express Edition
      vect[i] time      : 1356811
      vec.at(i) time    : 7760148
      *it = time        : 4913112
      for_each = time   : 455713
      arr[i] time       : 446280
      *ptr time         : 429595
      
      //GCC
      vect[i] time      : 431039
      vec.at(i) time    : 2421283
      *it = time        : 381400
      for_each = time   : 380972
      arr[i] time       : 363563
      *ptr time         : 365971  
      

      有趣的是,VC++ 中的迭代器和运算符[] 与 for_each 相比要慢得多(这似乎通过一些模板魔术来降低迭代器指向指针的性能)。
      在 GCC 中,at() 的访问时间更糟,这是正常的,因为它是测试的唯一范围检查函数。

      【讨论】:

      • 几乎没有任何统计差异。当实际使用实际程序时,大约 10% 的任何值都不会产生影响,除非这是在一个经常使用的紧密循环中。缓存未命中,时间相等
      • 如果你定义#define _SECURE_SCL 0 #define _SECURE_SCL_THROWS 0 指针和迭代器的性能没有区别。
      【解决方案8】:

      这取决于容器的类型。对于vector,您使用哪个可能并不重要。方法 0 变得更加惯用了,但正如每个人所说,它们并没有太大的区别。

      如果您决定使用list,则方法1 原则上应为O(N),但实际上没有列表at() 方法,因此您甚至不能那样做。 (所以在某种程度上,你的问题堆得满满当当。)

      但这本身就是方法 0 的另一个参数:它对不同的容器使用相同的语法。

      【讨论】:

        【解决方案9】:

        上面没有考虑的一种可能性:根据“做某事”的细节,可以同时有方法0和方法1,你不必选择:

        for (auto i = elemVec.begin(), ii = 0; ii < elemVec.size(); ++i, ++ii)
        {
            // Do something with either the iterator i or the index ii
        }
        

        这样,无论是查找索引还是访问对应的成员,都很简单。

        【讨论】:

          猜你喜欢
          • 2011-06-22
          • 1970-01-01
          • 1970-01-01
          • 2013-04-16
          • 1970-01-01
          • 2011-12-25
          • 1970-01-01
          • 2014-08-13
          • 1970-01-01
          相关资源
          最近更新 更多