我在a set of slides遇到了关于C ++的 rant 谈话。这里和那里有一些有趣的花絮,但是幻灯片8对我来说很突出。其内容大约是:
不断变化的风格
老而破坏:
for (int i = 0; i < n; i++)
新热点:
for (int i(0); i != n; ++i)
我之前从未见过使用第二种形式的for
循环,所以声称它是“新热点”让我感兴趣。我可以看到它的一些基本原理:
!=
的硬件速度可能比<
++i
不要求编译器保留i
的旧值,这就是i++
会做的事情。我认为这是不成熟的优化,因为现代优化编译器会将两者编译成相同的指令;如果有的话,后者更糟糕,因为它不是一个“正常”for
循环。使用!=
代替<
对我来说也是可疑的,因为它使循环在语义上与原始版本不同,并且可能导致一些罕见但有趣的错误。
for
循环的“新热度”版本是否受欢迎?这些天是否有任何理由使用该版本(2016年以上),例如不寻常的循环变量类型?
答案 0 :(得分:75)
使用构造函数与复制初始化直接初始化
这些与int
完全相同,并会生成相同的代码。使用您喜欢阅读的内容或代码政策,等等。
快
!=
的硬件速度可能比<
生成的代码实际上不是i < n
vs i != n
,而是i - n < 0
vs i - n == 0
。也就是说,您将在第一种情况下获得jle
,在第二种情况下获得je
。所有jcc
指令都具有相同的效果(请参阅instruction set reference和optionization reference,它们只列出所有jcc
指令,因为吞吐量为0.5)。
哪个更好?对于int
来说,在性能方面可能并不重要。
如果您想要跳过中间的元素,那么执行<
会更加安全,从那以后您就不必担心会遇到无限/未定义的循环。但是,只需编写使用您正在编写的循环编写最有意义的条件。另请查看dasblinkenlight's answer。
是的,这是胡说八道。如果您的编译器无法告诉您不需要旧值,只需将
++i
并不要求编译器保留i
的旧值,这就是i++
会做的事情。
i++
重写为++i
,那么请获取新的编译器。那些肯定会编译成具有相同性能的相同代码。
那就是说,使用正确的东西是一个很好的指导方针。您想增加i
,以便++i
。只在需要使用后增量时才使用后增量。完全停止。
那就是真正的新热点&#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 ++那样有效地处理后缀运算符++当它们仅用于副作用时。
(当然,如果您遇到只支持前缀++
的迭代器,那么就这样写。)