这个替代'for'循环语法有什么基础吗?

时间:2016-09-14 15:35:11

标签: c++ for-loop

我在a set of slides遇到了关于C ++的 rant 谈话。这里和那里有一些有趣的花絮,但是幻灯片8对我来说很突出。其内容大约是:

  

不断变化的风格

     

老而破坏:

for (int i = 0; i < n; i++)
     

新热点:

for (int i(0); i != n; ++i)

我之前从未见过使用第二种形式的for循环,所以声称它是“新热点”让我感兴趣。我可以看到它的一些基本原理:

  1. 使用构造函数与复制初始化直接初始化
  2. !=的硬件速度可能比<
  3. ++i不要求编译器保留i的旧值,这就是i++会做的事情。
  4. 我认为这是不成熟的优化,因为现代优化编译器会将两者编译成相同的指令;如果有的话,后者更糟糕,因为它不是一个“正常”for循环。使用!=代替<对我来说也是可疑的,因为它使循环在语义上与原始版本不同,并且可能导致一些罕见但有趣的错误。

    for循环的“新热度”版本是否受欢迎?这些天是否有任何理由使用该版本(2016年以上),例如不寻常的循环变量类型?

7 个答案:

答案 0 :(得分:75)

  1.   

    使用构造函数与复制初始化直接初始化

    这些与int完全相同,并会生成相同的代码。使用您喜欢阅读的内容或代码政策,等等。

  2.   

    !=的硬件速度可能比<

    生成的代码实际上不是i < n vs i != n,而是i - n < 0 vs i - n == 0。也就是说,您将在第一种情况下获得jle,在第二种情况下获得je。所有jcc指令都具有相同的效果(请参阅instruction set referenceoptionization reference,它们只列出所有jcc指令,因为吞吐量为0.5)。

    哪个更好?对于int来说,在性能方面可能并不重要。

    如果您想要跳过中间的元素,那么执行<会更加安全,从那以后您就不必担心会遇到无限/未定义的循环。但是,只需编写使用您正在编写的循环编写最有意义的条件。另请查看dasblinkenlight's answer

  3.   

    ++i并不要求编译器保留i的旧值,这就是i++会做的事情。

    是的,这是胡说八道。如果您的编译器无法告诉您不需要旧值,只需将i++重写为++i,那么请获取新的编译器。那些肯定会编译成具有相同性能的相同代码。

    那就是说,使用正确的东西是一个很好的指导方针。您想增加i,以便++i。只在需要使用后增量时才使用后增量。完全停止。

  4. 那就是真正的新热点&#34;肯定会是:

    for (int i : range(n)) { ... }
    

答案 1 :(得分:22)

您对优化编译器和前缀与后缀++运算符是正确的。对int无关紧要,但使用迭代器时更重要。

你问题的第二部分更有趣:

  

使用!=代替<对我来说也很可疑,因为它使循环在语义上与原始版本不同,并且可能导致一些罕见但有趣的错误。

我会将其改写为&#34;可以捕捉一些罕见但有趣的错误。&#34;

Dijkstra在他的书A Discipline of Programming中提出了一个简单的方法来论证这一点。他指出,推理后置条件较强的循环比推理后置条件较弱的循环更容易。由于循环的后置条件是其连续条件的倒数,因此应该优先选择具有较弱连续条件的循环。

a != b弱于a < b,因为a < b表示a != b,但a != b并不表示a < b。因此,a != b可以提供更好的延续条件。

简单来说,您知道在a == b的循环结束后,a != b结束了;另一方面,当a < b的循环结束时,你所知道的只是a >= b,这不如知道确切的相等。

答案 2 :(得分:13)

我个人不喜欢第二个,我会用:

for (int i = 0; i < n; ++i); //A combination of the two :)

int i = 0 vs int i(0)

没有任何区别,他们甚至编译成相同的汇编指令(没有优化):

int main()
{
    int i = 0; //int i(0);
}

int i = 0版本:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

int i(0)版本:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

i < n vs i != n

你是对的,!=可能会引入一些有趣的错误:

for (int i = 0; i != 3; ++i)
{
    if (i == 2)
        i += 2; //Oops, infinite loop
    //...
}

!=比较主要用于迭代器,它不会定义<(或>)运算符。也许这就是作者的意思?

但是在这里,第二个版本显然更好,因为它比其他版本更清楚地表明了意图(并且引入了更少的错误)。

i++ vs ++i

对于内置类型(和其他普通类型),例如int,没有区别,因为编译器会优化临时返回值。在这里,一些迭代器也很昂贵,因此创建和破坏可能会影响性能。

真的在这种情况下并不重要,因为即使没有优化它们也会发出相同的汇编输出!

int main()
{
    int i = 0;
    i++; //++i;
}

i++版本:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    addl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

++i版本:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    addl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

答案 3 :(得分:7)

这两种形式与表现无关。重要的是你如何编写代码。遵循类似的模式,专注于表达和简洁。因此,对于初始化,首选int i(0)(或更好:i {0}),因为这强调这是初始化,而不是赋值。为了比较,!=和&lt;之间的区别。是你对你的类型提出了较低的要求。对于整数没有区别,但总的来说迭代器可能不会支持少于,但应该始终支持相等。最后,前缀增量更好地表达了你的意图,因为你没有使用结果。

答案 4 :(得分:3)

在这段代码中没有区别。但我想作者的目标是为所有for循环使用相同的样式编码样式(可能是基于范围的循环除外)。

例如,如果我们将其他类作为循环计数器类型

for ( Bla b = 0; b < n; b++ )

然后有问题:

    如果Bla b = 0;没有可访问的副本/移动构造函数,则
  • Bla可能无法编译
  • 如果b < n不允许定义弱排序或定义Bla,则
  • operator<可能无法编译。
  • b++可能无法编译或出现意外的副作用,因为后增量通常按值返回

使用编写器的建议模式,减少了对循环迭代器的要求。

请注意,此讨论可以永久进行。我们可以说int i{};更好,因为某些类型可能不会将0作为初始化程序。那么我们可以说,如果n不是int,那该怎么办?它应该是decltype(n) i{}。或者实际上我们应该使用基于范围的循环来解决所有上述问题。等等。

所以在一天结束时仍然是个人偏好。

答案 5 :(得分:3)

  

我!= n

坦率地说,幻灯片8在那里失去了我,你怀疑某些事情可能不太正确。

除了现代编译器将这些类型的循环完全优化的可能性,因为它在理论上可能适用于当前的CPU,因此鼓励程序员编写不太健壮的代码以帮助优化器&#34;对于无论什么原因只是非常古老而且在现代世界中没有的地方,IMO。软件的实际成本是人类时间,而不是CPU时间,占所有项目的99.99%。

在元级别上,真理在编码指南中没有位置。建立编码惯例的幻灯片没有给出客观和深思熟虑的原因是没有用的。请注意,我会这样做,因为&#34;我们这样做是因为我们想选择一个样式而不是过多#34;我不需要技术原因 - 只是任何原因。然后,我可以决定是否接受编码指南背后的推理(因此接受指南本身)。

答案 6 :(得分:0)

在可行的情况下,在C风格的for循环中使用postfix增量/减量更好风格

for (i = 0; i < n; i++)

而不是

for (i = 0; i < n; ++i)

使用后缀运算符,循环变量i出现在所有三个表达式的左侧,这使代码更易于阅读。此外,如果您需要逐步执行除1以外的任何值,则必须使用左侧的i进行编写...

for (i = 0; i < n; i += 2)

因此,为了保持一致性,当您使用++ += 1的简写时,也应该这样做。 (正如评论中指出的那样,如果我们没有45年的会议教导我们+= 1,我们可能会发现++++更容易阅读。)

对于像int这样的基本类型,前缀/后缀增量不会产生性能差异;正如其他答案所证明的那样,编译器将以任何一种方式生成完全相同的代码。使用迭代器,优化器必须做更多工作才能使它正确,但我不认为C ++ 11时代的编译器有任何借口无法像前缀operator ++那样有效地处理后缀运算符++当它们仅用于副作用时。

(当然,如果您遇到只支持前缀++的迭代器,那么就这样写。)