【问题标题】:++it or it++ when iterating over a map?迭代地图时++it或it++?
【发布时间】:2011-08-03 13:08:43
【问题描述】:

显示如何迭代 std::map 的示例通常是这样的:

MapType::const_iterator end = data.end(); 
for (MapType::const_iterator it = data.begin(); it != end; ++it)

即它使用++it 而不是it++。有什么理由吗?如果我改用it++会不会有什么问题?

【问题讨论】:

  • 看看[这个][1]。 [1]:stackoverflow.com/questions/24853/…
  • 如果您在 C++ 中编写任何时间长度的代码,++it 将显着提高效率。不是在你的代码中,而是花时间向人们解释你为什么不首先做++it。既然真的没有半点关系,那还不如用不会引起争论的方式来做。
  • @Dennis:没关系很多,而且通常无法衡量,但在紧密的循环中,它可以产生影响。
  • @Christopher:人们这么说,但没有人举过例子。我曾经运行过的每个测试都导致生成的代码几乎相同,即使在“紧密循环”中也不值得担心。唯一的例外是当增量运算符足够复杂以至于无法内联时[因此没有删除无关的临时]。
  • @Dennis:我应该有更多的资格,是的。我说的是复杂迭代器的异常情况。

标签: c++ iterator iteration stdmap


【解决方案1】:

it++ 返回前一个迭代器的副本。由于没有使用此迭代器,因此这是一种浪费。 ++it 返回对递增迭代器的引用,避免复制。

请参阅Question 13.15 以获得更完整的说明。

【讨论】:

  • 这无关紧要。这个问题纯粹是风格问题。
  • @Dark - 因为它没有被使用,编译器很可能会注意到这一点并优化掉副本。
  • 我知道这一点。然而,显然编译器不需要这样做。
  • 我个人觉得预增量读起来更好。将“++”读作“增量”,它在你的脑海中变成“增量”,而不是“它增量”
  • 我不会说这是风格问题。预增量说,“增量”。后增量说,“增量,但给我原来的价值,这样我就可以用它做点什么。”如果您没有使用原始值做某事,那么后增量并不能清楚地表达您的意图。
【解决方案2】:

测试了一下,我做了三个源文件:

#include <map>

struct Foo { int a; double b; char c; };

typedef std::map<int, Foo> FMap;

### File 1 only ###

void Set(FMap & m, const Foo & f)
{
  for (FMap::iterator it = m.begin(), end = m.end(); it != end; ++it)
    it->second = f;
}

### File 2 only ###

void Set(FMap & m, const Foo & f)
{
  for (FMap::iterator it = m.begin(); it != m.end(); ++it)
    it->second = f;
}

### File 3 only ###

void Set(FMap & m, const Foo & f)
{
  for (FMap::iterator it = m.begin(); it != m.end(); it++)
    it->second = f;
}

### end ###

在使用g++ -S -O3、GCC 4.6.1 编译后,我发现版本 2 和 3 产生了相同的程序集,而版本 1 仅在一条指令上有所不同,cmpl %eax, %esicmpl %esi, %eax

所以,随你挑选,使用适合你风格的任何东西。前缀增量++it 可能是最好的,因为它最准确地表达了您的要求,但不要为此而烦恼。

【讨论】:

  • 很好的优化展示,但这只适用于足够简单和内联的operator++。很高兴看到 g++ 可以为 std::map 做到这一点。
  • @Christopher: 是的,你绝对应该总是使用++it,这就是你的意思。我只是想对两个经常宣传的点的真正影响进行一些硬比较(提升末端并使用前缀增量)。
【解决方案3】:

performance advantage 在使用前自增运算符和后自增运算符方面略有不同。在设置使用迭代器的循环时,您应该选择使用预增量:

for (list<string>::const_iterator it = tokens.begin();
    it != tokens.end();
    ++it) { // Don't use it++
    ...
}

当您考虑通常如何实现这两个运算符时,原因就会显现出来。预增量非常简单。但是,为了使后增量起作用,您需要首先制作对象的副本,对原始对象进行实际增量,然后返回副本:

class MyInteger {
private:
    int m_nValue;

public:
    MyInteger(int i) {
        m_nValue = i;
    }

    // Pre-increment
    const MyInteger &operator++() {
        ++m_nValue;
        return *this;
    }

    // Post-increment
    MyInteger operator++(int) {
        MyInteger clone = *this; // Copy operation 1
        ++m_nValue;
        return clone; // Copy operation 2
    }
}

如您所见,后增量实现涉及两个额外的复制操作。如果所讨论的对象体积庞大,这可能会非常昂贵。话虽如此,一些编译器可能足够聪明,可以通过优化摆脱一次复制操作。关键是后增量通常比前增量涉及更多的工作,因此习惯于将“++”放在迭代器之前而不是之后是明智的。

(1) 归功于链接的网站。

【讨论】:

    【解决方案4】:

    从逻辑的角度来看 - 这是相同的,没关系在这里

    为什么使用前缀 one - 因为它更快 - 它更改迭代器并返回其值,而后缀创建临时对象,递增当前迭代器,然后返回临时对象(相同迭代器的副本,在递增之前)。由于没有人在这里观察这个临时对象(返回值),所以它是相同的(逻辑上)。

    编译器很有可能会对此进行优化。


    此外 - 实际上,对于 any 类型应该是这样的。但它应该是。由于任何人都可以重载operator++ - 后缀和前缀,它们可能会产生副作用和不同的行为。

    好吧,这是一件可怕的事情,但仍有可能。

    【讨论】:

    • 确实是一件可怕的事情。比如滥用 shift 运算符将数据放入流中,对吗? ;-) (SCNR)
    • 嗯,这是一个很好的观点——毕竟,您最终可能会处理一个自定义迭代器,无论出于何种原因复制该迭代器的成本都非常高,并且无法优化,所以只是习惯性地写下您的意思(++it) 而不是您可能已经习惯的 (c++) 只是很好的做法。
    • @Kerrek SB - 完全正确:) 我总是更喜欢++bla_bla,只要可以使用。
    【解决方案5】:

    不会造成任何问题,但使用++it 更正确。对于小类型,使用++ii++ 并不重要,但对于“大”类:

    operator++(type x,int){
        type tmp=x; //need copy
        ++x;
        return tmp;
    }
    

    编译器可能会优化掉其中的一些,但很难确定。

    【讨论】:

      【解决方案6】:

      正如其他答案所说,除非它在上下文中不起作用,否则更喜欢 ++it。对于迭代小类型的容器,它确实没有什么区别(或者如果编译器将其优化掉,则没有区别),但对于大类型的容器,由于节省了制作副本的成本,它可以有所作为。

      确实,您可能知道在您的特定上下文中该类型足够小,因此您不必担心。但稍后,您团队中的其他人可能会将容器的内容更改为重要的位置。另外,我认为最好让自己养成一个好习惯,并且只有在你知道必须的时候再增加。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-11-07
        • 1970-01-01
        • 2012-10-29
        • 2011-04-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多