这种替代的"for"循环语法有什么依据吗?

Is there any basis for this alternative 'for' loop syntax?

本文关键字:什么 语法 循环 for      更新时间:2023-10-16

我遇到了一组关于c++的rant演讲的幻灯片。这里和那里有一些有趣的花絮,但幻灯片8对我来说很突出。其内容大致为:

千变万化的风格

又老又破:

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

新热点:

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

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

  1. 使用构造函数直接初始化与复制初始化
  2. !=在硬件上比<
  3. 更快
  4. ++i不需要编译器保持i的旧值,这是i++会做的。

我认为这是不成熟的优化,因为现代优化编译器会将这两个编译成相同的指令;如果有什么区别的话,后者更糟糕,因为它不是"正常的"。for循环。使用!=而不是<对我来说也很可疑,因为它使循环在语义上与原始版本不同,并且可能导致一些罕见但有趣的错误。

有没有什么时候"新热点"版本的for循环很受欢迎?有什么理由使用这个版本这些天(2016+),例如不寻常的循环变量类型?

  1. 使用构造函数直接初始化vs .复制初始化

    这些对于int s是完全相同的,并且将生成相同的代码。使用你喜欢阅读的或者你的代码策略,等等。

  2. !=在硬件上可能比<更快

    生成的代码实际上不是i < n vs i != n,而是i - n < 0 vs i - n == 0。也就是说,您将在第一种情况下得到jle,在第二种情况下得到je。所有jcc指令都具有相同的性能(请参阅指令集参考和选项化参考,它们只是列出所有jcc指令的吞吐量为0.5)。

    哪个更好?对于int s,可能并不重要的性能方面。

    如果你想跳过中间的元素,执行<更安全,因为这样你就不必担心会出现无限/未定义的循环。但是只要写出对你正在写的循环最有意义的条件。还可以看看dasblinkenlight的回答

  3. ++i不需要编译器保留i的旧值,而i++则需要。

    那是胡说八道。如果你的编译器不能告诉你不需要旧的值,只是将i++重写为++i,然后获得一个新的编译器。它们肯定会编译成相同的代码,具有相同的性能。

    也就是说,使用正确的东西是一个很好的指导方针。你想增加i,所以这是++i。只有在需要使用后增量时才使用后增量。句号。


也就是说,真正的"新热点"肯定是:

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

关于优化编译器和前缀与后缀++操作符,您是正确的。这对int来说并不重要,但在使用迭代器时就更重要了。

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

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

我将把它重新表述为"可以捕获一些罕见但有趣的bug。"

Dijkstra在他的书A Discipline of Programming中提供了一个简单的方法来论证这一点。他指出,对后置条件较强的循环进行推理要比对后置条件较弱的循环进行推理容易。由于循环的后置条件与其延续条件相反,因此应该选择延续条件较弱的循环。

a != ba < b弱,因为a < b暗示a != b,但a != b并不暗示a < b。因此,a != b是较好的延续条件。

用非常简单的术语来说,你知道a == b紧接a != b的循环结束;另一方面,当使用a < b的循环结束时,您所知道的只是a >= b,这不如知道确切的相等性。

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

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) version:

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

i < n vs i != n

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

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++ version:

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

这两个表单与性能没有任何关系。重要的是如何编写代码。遵循类似的模式,注重表达和简洁。因此,对于初始化,首选int i(0)(或者更好:i{0}),因为这强调了这是初始化,而不是赋值。对于比较,!=和<你对你的类型要求较低。对于整数没有区别,但通常迭代器可能不支持小于,但应该始终支持相等。最后,前缀增量更好地表达了您的意图,因为您不使用结果。>

在此代码中没有区别。但我猜作者的目的是为所有的for循环(除了基于范围的循环,大概)使用相同的风格编码风格。

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

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

那就有问题了:

    如果Bla没有可访问的复制/移动构造函数,
  • Bla b = 0;可能编译失败
  • b < n编译失败,如果Bla不允许定义弱排序,或者定义operator<
  • b++可能无法编译或有意想不到的副作用,因为后增量通常返回值

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


请注意,这个讨论可以永远持续下去。我们可以说int i{};更好,因为有些类型可能不承认0作为初始化项。然后我们可以说,如果n不是int呢?应该是decltype(n) i{}。事实上,我们应该使用基于范围的循环来解决上述所有问题。等等。

所以在一天结束的时候,这仍然是个人的喜好。

i = n

坦率地说,幻灯片8把我弄糊涂了,你的怀疑是对的,有些地方可能不太对。

除了现代编译器很有可能对这些循环进行彻底的优化,因为这在理论上是可能的,对于当前的cpu,鼓励程序员为了而写一些不那么健壮的代码来"帮助优化器",无论的原因是什么,这都是非常过时的,在现代世界中根本没有的位置。现在软件的真正成本是人力时间,而不是CPU时间,99.99…

在元层面上,真理在编码指南中没有位置。一个建立编码惯例的幻灯片,如果没有给出客观和深思熟虑的理由,那是没有用的。请注意,我可以接受这样的理由:"我们这样做是因为我们想选择一种风格,而不是过多的风格",我不需要一个技术上的理由——只要任何原因。然后我可以决定是否接受编码指南背后的推理(从而接受指南本身)。

从风格上来说在c风格的for循环中尽可能使用后缀自增/自减是比较好的

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时代的编译器没有任何借口不能像处理前缀操作符++那样有效地处理后缀操作符++,当它们仅用于其副作用时。

(当然,如果您坚持使用只支持前缀++的迭代器,那么您可以这样写。)