【问题标题】:Caching the end iterator - Good idea or Bad Idea?缓存结束迭代器——好主意还是坏主意?
【发布时间】:2011-03-06 06:36:04
【问题描述】:

一般来说,出于效率和速度目的缓存结束迭代器(特别是 STL 容器)是个好主意吗?比如下面这段代码:

std::vector<int> vint;
const std::vector<int>::const_iterator end = vint.end();
std::vector<int>::iterator it = vint.begin();

while (it != end)
{
   ....
   ++it;
}

什么情况下最终值会失效?从容器中擦除是否会导致 end 在 所有 STL 容器或仅部分容器中无效?

【问题讨论】:

  • 首先问问自己,您的分析器是否告诉您调用 std::vector::end() 会花费大量的处理时间?

标签: c++ stl iterator containers


【解决方案1】:

vector 的简单情况下,end 迭代器将在您从容器中添加或删除元素时发生变化;但是,通常最安全的假设是,如果您在迭代容器时对其进行了变异,则它的 all 迭代器将变为无效。在任何给定的 STL 实现中,迭代器的实现方式可能不同。

关于缓存 end 迭代器 - 缓存它当然是有效的,但要确定它是否在您的情况下实际上更快,最好的选择是您分析您的代码并查看。虽然从 vector 检索 end 迭代器可能是使用最近的 STL 库和编译器的快速实现,但我在过去的项目中工作,缓存 end 迭代器给了我们显着的速度促进。 (这是在 PlayStation 2 上使用的,所以请谨慎对待。)

【讨论】:

    【解决方案2】:

    如果我们谈论效率和速度:缓存结束迭代器是不必要的,因为编译器优化和内联。

    【讨论】:

    • 真的吗?我看到的大多数关于使用 STL 算法(如 for_each)的建议是,它会缓存 end 迭代器,因此不会在每次迭代时计算它。
    • @Matthieu:迭代器不能保证在某些容器的修改后仍然存在(例如,调整向量的大小会使向量的 all 迭代器无效)。 for_each 不给函数对象访问迭代器,容器不能改变,因此缓存迭代器是可以的。但是,如果您欺骗系统并从函数对象以使迭代器无效的方式修改容器,那么当通过现在无效的迭代器访问容器时,显然会导致 未定义的行为,可能会崩溃。
    • 实际上,调整向量的大小只会在发生重新分配时使迭代器无效,如果您仅调整大小到容量,您可以确保它不会。但是我的问题仍然存在,我不确定编译器是否能够优化 end() 在循环外的计算,即使将编译器内联为先验也无法知道该值是否可能被修改据我所知,循环内的操作。但是我对编译器优化知之甚少,因此我的问题是:)
    • @Matthieu:您可以查看为您的循环生成的代码 (stackoverflow.com/questions/840321/…)。在那里你会发现各种方法之间的区别。
    【解决方案3】:

    从您当前正在迭代的容器中擦除总是一个坏主意。最终迭代器的实际缓存不会改变这一点。

    h.

    【讨论】:

    • 完全没有,如果你做得好的话。例如,std::vector 的 'erase' 成员返回一个适当的新迭代器。因此,只要您正确编写代码(也就是说,记住其他迭代器现在无效),您从正在迭代的容器中擦除不会有任何问题。
    【解决方案4】:

    一般来说这是个好主意 缓存一个结束迭代器(特别是 STL 容器)以提高效率和 速度目的?

    如果您使用 STL 容器算法,最终迭代器的缓存无论如何都会发生(当您将 container.end() 的结果作为参数传递时)。

    如果您修改容器的内存(插入/删除元素),这是个坏主意。

    另外,缓存效率很少有多大意义:在大多数情况下 end() 由编译器内联,如果不是,很可能你的效率不会挂在 end() 结果是缓存。虽然是 YMMV。

    【讨论】:

      【解决方案5】:

      对于每种类型的容器,无效规则(针对迭代器)都非常明确地定义。我发现 SGI 网站非常有用http://www.sgi.com/tech/stl/table_of_contents.html

      特别是我发现的向量:

      [5] 向量的迭代器在其内存被重新分配时失效。此外,在向量中间插入或删除元素会使指向插入或删除点之后元素的所有迭代器无效。因此,如果您使用 reserve() 预分配向量将使用的尽可能多的内存,并且所有插入和删除都在向量的末尾,则可以防止向量的迭代器失效。

      【讨论】:

        【解决方案6】:

        我经常使用这种风格来迭代容器:

        // typedef std::vector<Person> Persons;
        Persons::iterator it = persons.begin(), end = persons.end();
        for (; it != end; ++it)
        {
            Person & person = *it;
            // ...
        }
        

        从向量中删除元素会使删除位置之后的所有迭代器无效。

        我不确定其他容器类型。无论如何,我认为可以安全地假设所有迭代器在擦除后都变得无效。如果您真的需要非常具体的信息,那么您可以随时查找。我很少需要这个,因为我的编码风格相当保守。

        【讨论】:

          【解决方案7】:

          一般来说,是否缓存结束迭代器并不重要。如果您觉得这很重要,那么您应该已经在您的代码上使用了分析器,并且能够分析这两种变体。我怀疑它可能会因容器类型而异——但考虑到您的编译器、优化和 STL 供应商,探查器将是唯一确定的方法。

          【讨论】:

            【解决方案8】:

            这真的,真的取决于你在... 代码中做了什么。

            如果编译器可以证明vint.end() 不会改变,那么这可能无关紧要,但是您将受制于编译器优化以及... 代码的清晰程度。

            您的方法对编译器的帮助最大,您承诺end 迭代器不会失效,并且您不会修改end 中的元素(无论如何都是无效的)。对于您在... 中的承诺,您不能比这更明确。

            现在是 2019 年,for-range 循环基本等同于你的代码:https://en.cppreference.com/w/cpp/language/range-for

            {
                auto && __range = range_expression ;
                auto __begin = begin_expr ;
                auto __end = end_expr ;
                for ( ; __begin != __end; ++__begin) {
            
                    range_declaration = *__begin;
                    loop_statement
            
                }
            }
            

            顺便说一句,如果您对it 并不真正感兴趣,但对*it 感兴趣,您可以结束(不是双关语)这个困境并写:

            std::vector<int> vint;
            for(auto&& e : vint)
            {
               .... // use `e` instead of `*it`.
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2011-10-21
              • 2015-07-20
              • 2014-02-13
              • 1970-01-01
              • 2010-11-23
              • 1970-01-01
              • 2011-10-26
              相关资源
              最近更新 更多