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

awk*_*ksp 45 c++ for-loop

我遇到了一组用于C++ 咆哮的幻灯片.这里和那里有一些有趣的花絮,但是幻灯片8对我来说很突出.其内容大约是:

不断变化的风格

旧的和破坏的:

for (int i = 0; i < n; i++)
Run Code Online (Sandbox Code Playgroud)

新热度:

for (int i(0); i != n; ++i)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Bar*_*rry 74

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

    这些与ints 完全相同,并将生成相同的代码.使用您喜欢阅读的任何一个或您的代码政策,等等.

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

    生成的代码实际上不会i < nVS i != n,它会像i - n < 0VS i - n == 0.也就是说,你会jle在第一种情况下获得a ,je在第二种情况下获得a .所有jcc指令都具有相同的性能(请参阅指令集参考选项参考,它们只是将所有jcc指令列在一起,因为吞吐量为0.5).

    哪个更好?对于ints,可能与性能无关.

    <如果你想跳过中间的元素,那么要非常安全,从那以后你不必担心最终会出现无限/未定义的循环.但是,只需编写使用您正在编写的循环编写最有意义的条件.另请查看dasblinkenlight的答案.

  3. ++i不需要编译器保持旧的值i,这是i++可以做的.

    是的,这是胡说八道.如果你的编译器不能告诉你不需要旧的值而只是重写i++to ++i,那么就得到一个新的编译器.那些肯定会编译成具有相同性能的相同代码.

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


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

for (int i : range(n)) { ... }
Run Code Online (Sandbox Code Playgroud)

  • @awksp它们总是相同的,除非构造函数是显式的,在这种情况下,一个将编译而另一个不会. (4认同)
  • @Nawaz大声笑你在我即将投票之前删除你的帖子就像1s一样... (2认同)

das*_*ght 22

你是正确的优化编译器和前缀与后缀++运算符.它对于一个并不重要int,但是当你使用迭代器时它更重要.

问题的第二部分更有趣:

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

我将它改写为"可以捕获一些罕见但有趣的错误."

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

a != b是弱a < b,因为a < b暗示a != b,但a != b并不意味着a < b.因此,a != b做出更好的延续条件.

简单来说,你知道a == b循环结束后会立即a != b结束; 另一方面,当循环a < b结束时,所有你知道的是a >= b,这不如知道确切的相等.

  • 然而,这是相当学术性的 - 如果你曾经遇到过'a> b`会导致循环后问题的情况,那么延续条件`a!= b`就已经导致了无限循环. (9认同)
  • @OliverCharlesworth这就是重点!无限循环比计算的(略微)错误结果更有吸引注意力的机会.我认为这个特定的论点是一种罕见的情况,当一个纯粹的学术论证为你提供免费的,易于遵循的日常编码建议.FWIW,我用这种方式编写循环已有28年了. (5认同)
  • `当你使用迭代器时,它更重要 - 这是我缺乏经验的一个明确迹象,我在编写这个问题时根本没有考虑过迭代器.Dijkstra的说法对我来说是个新鲜事.我并不完全相信,因为虽然你知道循环之后的'a == b`,但你不知道'a`是"在范围内",因为缺少一个更好的短语.如果你能证明循环索引不能跳过`b`的值,那么它是否也适用于`a <b`?不能认为无限循环比逐个错误更明显 (3认同)
  • @awksp当你使用`!=`时,你的读者不必证明`a`不会跳过`b`,因为你在代码的主体中明确地写了它.另一方面,当您编写`<`时,您的读者可能需要检查循环体是否有其他技巧,例如将`a`设置为'MAX_INT`或同样愚蠢的东西.使用`!=`告诉他们没有技巧; `<`让读者自己检查. (3认同)

Rak*_*111 13

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

for (int i = 0; i < n; ++i); //A combination of the two :)
Run Code Online (Sandbox Code Playgroud)

int i = 0 VS int i(0)

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

int main()
{
    int i = 0; //int i(0);
}
Run Code Online (Sandbox Code Playgroud)

int i = 0 版:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

int i(0) 版:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

i < n VS i != n

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

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

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

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


i++ VS ++i

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

但是,这真的不会在这种情况下无所谓,因为即使没有优化他们发出相同组件的输出!

int main()
{
    int i = 0;
    i++; //++i;
}
Run Code Online (Sandbox Code Playgroud)

i++ 版:

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

++i 版:

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


use*_*426 7

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

  • "i {0}"的+1 - 这是真正的新热点:现在所有初始化都具有相同的外观,并且明显不同于赋值. (2认同)