有没有这种替代“for”循环语法的基础?

我偶然发现了一组幻灯片,用于讨论C ++的咆哮 。 在这里和那里有一些有趣的花絮,但幻灯片8站在我身边。 其内容大约是:

不断变化的风格

老和破坏:

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

新热点:

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

我以前从来没有见过使用第二种forms的for循环,所以声称这是“新的热度”对我感兴趣。 我可以看到一些理由:

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

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

有没有什么地方的for循环“新热”版本stream行? 这些date(2016+)是否有任何理由使用该版本,例如exception循环variablestypes?

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

    这些对于int是完全相同的,并且会生成相同的代码。 使用你喜欢阅读的内容或你的代码策略是什么等等

  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++ to ++i ,就可以得到一个新的编译器。 那些肯定会编译成相同的性能相同的代码。

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


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

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

关于优化编译器和前缀与后缀++运算符是对的。 对于int并不重要,但在使用迭代器时更重要。

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

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

我会把它改为“可以捕捉到一些罕见但有趣的错误”。

迪克斯特拉在他的书“规划学科 ”( 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 ,这不如知道确切的相等。

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

 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 //... } 

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

但在这里,第二个版本显然更好,因为它比另一个更明确地表示意图(并引入更less的错误)。


i++++i

对于内置types(以及其他普通types),如int ,没有任何区别,因为编译器会优化临时返回值。 在这里,一些迭代器又是昂贵的,所以创build和销毁可能是一个性能问题。

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

 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 

这两种forms与表演无关。 重要的是你如何编写代码。 遵循类似的模式,注重performance力和简洁。 因此,对于初始化,首选int i(0)(或更好:我{0}),因为这强调这是一个初始化,而不是一个赋值。 为了进行比较,!=和<的区别在于您对types的要求较低。 对于整数是没有区别的,但是一般来说迭代器可能不支持小于,但应该始终支持平等。 最后,前缀增量更好地expression你的意图,因为你不使用结果。

在这个代码中没有区别。 但是我猜想作者的目标是对所有的for循环使用相同的样式编码风格(除了基于范围的风格)。

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

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

那么就有问题了:

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

使用作者build议的模式,循环迭代器的需求减less。


请注意,这个讨论可以永远持续下去。 我们可以说int i{}; 是更好的,因为有些types可能不承认0作为初始值设定项。 那么我们可以说,如果n不是一个int ? 它应该是decltype(n) i{} 。 或者实际上我们应该使用基于范围的循环来修复所有上述问题。 等等。

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

我!= n

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

除了现代编译器具有这种循环优化的高度可能性之外,对于当前的CPU来说,这在理论上是可行的,所以鼓励程序员编写不太稳健的代码来“帮助优化器”, 无论出于何种原因,国际海事组织在现代世界根本没有地位。 软件的真正成本是现在的人工时间,而不是CPU时间,所有项目的99.99%。

在元层面上,真实性在编码准则中是没有地位的。 没有给出客观和深思熟虑的理由,build立一个编码惯例的幻灯片是没有用的。 请注意,我可以用“我们这样做,因为我们想select一种风格而不是过多”这样的理由,我不需要任何技术原因 – 只是任何理由。 然后,我可以决定是否接受编码指南背后的推理(因此接受指导本身)。

只要有可能,在C风格for循环中使用后缀递增/递减在风格上更好,

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

而不是

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

使用后缀运算符时,循环variablesi出现在所有三个expression式的左侧,这使代码更易于阅读。 另外,如果你需要除了1以外的任何值,你必须写在左边的i

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

所以为了一致性的缘故,当你使用+= 1++速记时也应该这样做。 (正如在评论中指出的那样,如果我们没有45年的惯例教我们++ ,我们可能会发现+= 1++更容易阅读。)

int这样的基本types,前缀/后缀增量不会影响性能; 正如其他答案已经certificate的那样,编译器会以任何方式生成完全相同的代码。 使用迭代器,优化器必须做更多的工作才能把它做好,但是我不认为C ++ 11时代的编译器没有任何理由不能像前缀运算符++那样有效地处理postfix operator ++,因为它们仅用于他们的副作用。

(当然,如果你用一个只支持前缀++的迭代器,那么你就是这样写的。)