在for循环中进行迭代器初始化被认为是不好的风格,为什么?

13

通常你会发现STL代码长这样:

for (SomeClass::SomeContainer::iterator Iter = m_SomeMemberContainerVar.begin(); Iter != m_SomeMemberContainerVar.end(); ++Iter)
{
}

但实际上我们建议这样写:

SomeClass::SomeContainer::iterator Iter = m_SomeMemberContainerVar.begin();
SomeClass::SomeContainer::iterator IterEnd = m_SomeMemberContainerVar.end();
for (; Iter != IterEnd; ++Iter)
{
}

如果你担心作用域问题,就加上封闭的大括号:

{
    SomeClass::SomeContainer::iterator Iter = m_SomeMemberContainerVar.begin();
    SomeClass::SomeContainer::iterator IterEnd = m_SomeMemberContainerVar.end();
    for (; Iter != IterEnd; ++Iter)
    {
    }
}

这样做可以提高速度和效率,特别是在编程控制台时,因为在循环的每次迭代中不会调用.end()函数。我认为性能提升是理所当然的,听起来合理,但我不知道它有多少提升,并且肯定取决于容器类型和实际使用的STL实现。但是我已经使用这种风格几个月了,实际上我更喜欢它而不是第一个例子。

原因在于可读性:for行非常整洁。在真正的生产代码中使用限定符和成员变量时,如果使用第一个示例中的风格,很容易出现非常长的for行。这就是为什么我故意让本示例具有水平滚动条,只是为了让你看到我在说什么。 ;)

另一方面,你突然将Iter变量引入到for循环的外部作用域。但是,在我工作的环境中,即使在第一个示例中,Iter也将在外部作用域中可访问。

你对此有何看法?除了可能限制Iter的范围外,第一个风格还有哪些优点吗?

13个回答

15

如果你将你的代码适当地分成多行,那么行内形式也会同样易读。此外,你应该始终将 iterEnd = container.end() 作为一种优化方法:

for (SomeClass::SomeContainer::iterator Iter = m_SomeMemberContainerVar.begin(),
    IterEnd = m_SomeMemberContainerVar.end();
    Iter != IterEnd;
    ++Iter)
{
}

更新:根据paercebal的建议修复了代码。


没错。C++0x将引入全新的关键字auto,使我们能够避免迭代器声明。请注意,我猜测你的代码不会编译,因为第二个“SomeClass::SomeContainer::iterator”声明有问题。应该改为for(m::iterator i =..., iEnd = ... - paercebal

9

另一种选择是使用foreach宏,例如boost foreach

BOOST_FOREACH( ContainedType item, m_SomeMemberContainerVar )
{
   mangle( item );
}

我知道在现代C++中,宏是不被鼓励使用的,但在auto关键字普及之前,这是我发现的最简洁易读、完全类型安全和快速的方法。您可以使用任何初始化样式来实现您的宏,以获得更好的性能。

链接页面上还有一条注释,建议将BOOST_FOREACH重新定义为foreach,以避免烦人的全大写。


5

如果在 for 循环后不需要迭代器,则第一种形式(在 for 循环内部)更好。它将其范围限制在 for 循环内。

我严重怀疑两种方式都没有任何效率提升。使用 typedef 可以使其更易读。

typedef SomeClass::SomeContainer::iterator MyIter;

for (MyIter Iter = m_SomeMemberContainerVar.begin(); Iter != m_SomeMemberContainerVar.end(); ++Iter)
{
}

我建议使用更短的名字;-)

@Ferrucio:这不是有效的C++代码 - 你可能正在使用旧版本的VC++,它没有遵循标准,但当你升级时你必须修复它。 - 1800 INFORMATION
真的,旧编译器可能仍然接受旧标准。使用VC++,您实际上可以选择打开/关闭它。 - Ferruccio
@steffenj,你的环境中“工作”的代码并不是标准的C++(直到2008年Visual C++还存在这个问题)。 - crashmstr
它在旧标准和新标准下均有效。只是在新标准下,范围被限制为 for 循环。在旧标准下,迭代器将可见直到 for 循环所在的作用域的末尾。 - Ferruccio
除非您在循环内调用函数。否则编译器无法将调用提升到end(),除非它能够证明该函数不会修改容器(在大多数情况下很难或不可能)。 - KeithB
显示剩余3条评论

4
不要在循环开始之前获取 iter.end(),这是一个不好的做法。如果您的循环更改了容器,则结束迭代器可能会失效。此外,end() 方法保证是 O(1) 的。
过早地进行优化是万恶之源。
另外,编译器可能比你想象的聪明。

如果你的循环改变了容器,那么增量迭代器也会失效,因此如果你正在使用这种类型的“foreach”循环,最好预先初始化结束条件。 - Chris Blackwell
不,您可以始终通过保存来自使迭代器无效的函数返回的迭代器来更新计数器迭代器。实际上,这就是您应该做的,并且这些返回值的真正原因。但是,没有任何理由预先保存结束迭代器。 - wilhelmtell

4

在使用g++进行-O2优化的情况下(只是为了更具体),我查看了这个问题。

对于std::vector、std::list和std::map(和它们的变体)生成的代码没有区别。而std::deque有一个微小的开销。

因此,从性能角度来看,一般来说差别不大。


1

我没有任何控制台经验,但在大多数现代C++编译器中,除了作用域的问题外,两个选项最终都是等效的。Visual Studio编译器通常会将条件比较放在隐式临时变量(通常是寄存器)中,即使在调试代码中也是如此。因此,虽然逻辑上看起来像是每次迭代都在进行end()调用,但优化后的编译代码实际上只会进行一次调用,而比较是每次循环时唯一要做的事情。

这在控制台上可能不是这种情况,但您可以反汇编循环以检查是否正在进行优化。如果是这样,那么您可以使用您喜欢或组织中标准的任何样式。


1

就我个人而言,我没有特别强烈的意见,但是迭代器生命周期会让我倾向于在 for 循环中使用。

然而,可读性可能会成为一个问题;通过使用 typedef 可以帮助解决这个问题,这样迭代器类型就更易管理了:

typedef SomeClass::SomeContainer::iterator sc_iter_t;

for (sc_iter_t Iter = m_SomeMemberContainerVar.begin(); Iter != m_SomeMemberContainerVar.end(); ++Iter)
{
}

不是很大的改进,但有一点点。

好主意。我自己用过几次,但最近没用过,所以记不得评论了。它使代码变得更加易读。 - Herms

1

这可能会导致代码不连贯,但我也喜欢将其提取到一个单独的函数中,并将两个迭代器传递给它。

doStuff(coll.begin(), coll.end())

并且拥有...

template<typename InIt>
void doStuff(InIt first, InIt last)
{
   for (InIt curr = first; curr!= last; ++curr)
   {
       // Do stuff
   }
 }

喜欢的事情:

  • 永远不必提及丑陋的迭代器类型(或考虑它是否为const或非const)
  • 如果在每次迭代中不调用end()有所收益,我就得到了它

不喜欢的事情:

  • 打乱了代码
  • 额外函数调用的开销。

但总有一天,我们会有lambda表达式!


1

我认为这并不是不好的风格。只需使用typedef来避免STL冗长和过长的行。

typedef set<Apple> AppleSet;
typedef AppleSet::iterator  AppleIter;
AppleSet  apples;

for (AppleIter it = apples.begin (); it != apples.end (); ++it)
{
   ...
}

斯巴达编程是缓解您的样式问题的一种方式。


0
如果您关心作用域,可以在初始化和循环周围加上大括号。通常我会在函数开头声明迭代器,并在整个程序中重复使用它们。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接