有没有这种替代“for”循环语法的基础?
我偶然发现了一组幻灯片,用于讨论C ++的咆哮 。 在这里和那里有一些有趣的花絮,但幻灯片8站在我身边。 其内容大约是:
不断变化的风格
老和破坏:
for (int i = 0; i < n; i++)
新热点:
for (int i(0); i != n; ++i)
我以前从来没有见过使用第二种forms的for
循环,所以声称这是“新的热度”对我感兴趣。 我可以看到一些理由:
- 使用构造函数直接初始化vs复制初始化
-
!=
可能比<
更快 -
++i
不需要编译器保持我的旧值,这是i++
会做的。
我可以想象,这是过早的优化,但现代优化编译器会将两者编译成相同的指令; 如果有的话,后者更糟糕,因为它不是一个“正常” for
循环。 使用!=
而不是<
也是可疑的,因为它使得循环在语义上与原始版本不同,并且可能导致一些罕见但有趣的错误。
有没有什么地方的for
循环“新热”版本stream行? 这些date(2016+)是否有任何理由使用该版本,例如exception循环variablestypes?
-
使用构造函数直接初始化vs复制初始化
这些对于
int
是完全相同的,并且会生成相同的代码。 使用你喜欢阅读的内容或你的代码策略是什么等等 -
!=
可能比<
更快生成的代码实际上并不是
i < n
vsi != n
,它会像i - n < 0
vsi - n == 0
。 也就是说,在第一种情况下,你会得到一个jle
,在第二个情况下,你会得到一个je
。 所有的jcc
指令具有相同的性能(请参阅指令集引用和选项参考 ,它们将所有的jcc
指令一起列出为吞吐量为0.5)。哪个更好? 对于
int
s,可能无关紧要。如果你想跳过中间的元素,那么你就不必担心会以无限的/未定义的循环结束。 但是只要写出最有意义的条件就可以用你正在写的循环来写。 另外看看dasblinkenlight的答案 。
-
++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 ++,因为它们仅用于他们的副作用。
(当然,如果你用一个只支持前缀++
的迭代器,那么你就是这样写的。)