是否有技术原因使用>(<)而不是!=当在'for'循环?

我几乎从未见过这样的for循环:

for (int i = 0; 5 != i; ++i)
{}

for循环中加1时,是否有技术原因使用><而不是!= ?还是说这只是一种惯例?

14857 次浏览

没有技术上的原因。但是这样可以降低风险、可维护性和更好地理解代码。

<>是比!=更强的限制,在大多数情况下(我甚至可以说在所有实际情况下)实现完全相同的目的。

有重复问题在这里;和一个有趣的回答

可能会发生变量i被设置为某个较大的值,如果你只使用!=操作符,你将陷入一个无休止的循环。

你可以有一些

for(int i = 0; i<5; ++i){
...
if(...) i++;
...
}

如果循环变量是由内部代码编写的,i!=5可能不会打破循环。这是检查不平等的更安全的方法。

编辑关于可读性。 不等式形式使用得更频繁。因此,这是非常快的阅读,因为没有什么特别的理解(大脑负荷减少,因为任务是普通的)。所以读者们利用这些习惯是很酷的

while (time != 6:30pm) {
Work();
}

现在是下午6:31分……该死,我下次回家的机会就是明天了!:)

这表明更强的限制可以降低风险,可能更直观地理解。

是的,是有原因的。如果你像这样写一个(基于索引的)for循环

for (int i = a; i < b; ++i){}

那么对于ab的任何值,它都能像预期的那样工作(即当a > b时为零迭代,而不是如果你使用i == b;时为无限迭代)。

另一方面,对于迭代器,您可以编写

for (auto it = begin; it != end; ++it)

因为任何迭代器都应该实现operator!=,但并不是每个迭代器都可以提供operator<

也是基于范围的for循环

for (auto e : v)

不只是花哨的糖,但它们可显著降低编写错误代码的机会。

除了许多人提到它降低了风险之外,它还减少了与各种标准库组件交互所需的函数过载的数量。例如,如果你想要你的类型可存储在std::set中,或者用作std::map的键,或者与一些搜索和排序算法一起使用,标准库通常使用std::less来比较对象,因为大多数算法只需要严格的弱排序。因此,使用<比较而不是!=比较成为一个好习惯(当然,这是有意义的)。

最后但并非最不重要的是,这被称为防御性编程,意思是总是采用最强的情况,以避免当前和未来的错误影响程序。

不需要防御性编程的唯一情况是已经通过前置和后端条件证明了状态(但是,证明这是所有编程中最具防御性的)。

正如你可以从其他众多的答案中看到的,有理由使用<而不是!=,这将有助于在边缘情况,初始条件,意外循环计数器修改等…

说实话,我认为再怎么强调惯例的重要性也不为过。对于本例,其他程序员很容易看到您正在尝试做什么,但会导致多看一眼。编程的工作之一就是让每个人都能读懂并熟悉它,所以不可避免地,当有人不得不更新/更改你的代码时,你不需要花很多精力就能弄清楚你在不同的代码块中做了什么。如果我看到有人使用!=,我会假设他们使用它而不是<是有原因的,如果它是一个大循环,我会查看整个事情,试图找出你做了什么,使这个必要……这是浪费时间。

当你最常使用!=符号时,迭代器是一个重要的例子:

for(auto it = vector.begin(); it != vector.end(); ++it) {
// do stuff
}

当然:在实践中,我将依赖range-for编写相同的代码:

for(auto & item : vector) {
// do stuff
}

但重点仍然是:通常使用==!=来比较迭代器。

使用<最常见的原因是约定。更多的程序员认为这样的循环是“当索引在范围内时”,而不是“直到索引到达末尾”。如果可以,遵守惯例是有价值的。

另一方面,这里的许多答案都声称使用<表单有助于避免错误。我认为在许多情况下,这只是帮助隐藏错误。如果循环索引应该达到最终值,而实际上却超出了它,那么就会发生您没有预料到的事情,这可能会导致故障(或者是另一个错误的副作用)。<可能会延迟bug的发现。!=更有可能导致失速,挂起,甚至崩溃,这将帮助你更快地发现错误。bug越早被发现,修复它的成本就越低。

注意,这个约定是数组和向量索引特有的。当遍历几乎任何其他类型的数据结构时,您将使用迭代器(或指针)并直接检查结束值。在这些情况下,你必须确保迭代器将达到而不是超过实际的结束值。

例如,如果你要遍历一个普通的C字符串,通常更常见的是这样写:

for (char *p = foo; *p != '\0'; ++p) {
// do something with *p
}

int length = strlen(foo);
for (int i = 0; i < length; ++i) {
// do something with foo[i]
}

首先,如果字符串非常长,第二种形式会更慢,因为strlen是字符串的另一次传递。

对于c++ std::string,您可以使用基于范围的for循环、标准算法或迭代器,即使长度是现成的。如果使用迭代器,约定使用!=而不是<,如下所示:

for (auto it = foo.begin(); it != foo.end(); ++it) { ... }

类似地,迭代树、列表或deque通常涉及观察空指针或其他哨兵,而不是检查索引是否保持在范围内。

我用“技术”这个形容词来表示语言行为/怪癖和编译器的副作用,比如生成代码的性能。

为此,答案是:no(*)。(*)表示“请查阅您的处理器手册”。如果您正在使用一些边缘情况RISC或FPGA系统,您可能需要检查生成了哪些指令以及它们的成本。但是如果你使用的是几乎任何传统的现代体系结构,那么lteqnegt之间的处理器级成本没有显著差异。

如果如果你在使用边缘情况,你会发现!=需要三个操作(cmpnotbeq)而不是两个(cmpblt xtr myo)。在这种情况下,还是RTM。

在大多数情况下,原因是防御性/强化,特别是在使用指针或复杂循环时。考虑

// highly contrived example
size_t count_chars(char c, const char* str, size_t len) {
size_t count = 0;
bool quoted = false;
const char* p = str;
while (p != str + len) {
if (*p == '"') {
quote = !quote;
++p;
}
if (*(p++) == c && !quoted)
++count;
}
return count;
}

一个不那么做作的例子是,你使用返回值来执行递增,接受来自用户的数据:

#include <iostream>
int main() {
size_t len = 5, step;
for (size_t i = 0; i != len; ) {
std::cout << "i = " << i << ", step? " << std::flush;


std::cin >> step;
i += step; // here for emphasis, it could go in the for(;;)
}
}

试试这个,输入值1,2,10,999。

你可以防止这种情况:

#include <iostream>
int main() {
size_t len = 5, step;
for (size_t i = 0; i != len; ) {
std::cout << "i = " << i << ", step? " << std::flush;
std::cin >> step;
if (step + i > len)
std::cout << "too much.\n";
else
i += step;
}
}

但你想要的可能是

#include <iostream>
int main() {
size_t len = 5, step;
for (size_t i = 0; i < len; ) {
std::cout << "i = " << i << ", step? " << std::flush;
std::cin >> step;
i += step;
}
}

还有一些惯例偏向于<,因为在标准容器中的排序通常依赖于operator<,例如,在几个STL容器中的哈希确定相等性通过以下语句

if (lhs < rhs) // T.operator <
lessthan
else if (rhs < lhs) // T.operator < again
greaterthan
else
equal

如果lhsrhs是用户定义的类,则编写此代码为

if (lhs < rhs) // requires T.operator<
lessthan
else if (lhs > rhs) // requires T.operator>
greaterthan
else
equal

实现者必须提供两个比较函数。因此<已经成为受欢迎的操作符。

我认为像这样的表达

for ( int i = 0 ; i < 100 ; ++i )
{
...
}

意图表达比is

for ( int i = 0 ; i != 100 ; ++i )
{
...
}

前者清楚地指出,条件是对一个范围上的排他性上界的测试;后者是退出条件的二进制测试。如果循环体是非平凡的,那么索引只在for语句本身中被修改可能并不明显。

编写任何类型的代码(通常)都有几种方法,在这种情况下恰好有两种方法(如果算上<=和>=,则为三种)。

在这种情况下,人们更喜欢>和<以确保即使在循环中发生了意想不到的事情(比如bug),它也不会无限循环(BAD)。例如,考虑下面的代码。

for (int i = 1; i != 3; i++) {
//More Code
i = 5; //OOPS! MISTAKE!
//More Code
}

如果我们使用(i <3),我们将不会陷入无限循环,因为它施加了更大的限制。

你是想让程序中出现一个错误来关闭整个程序,还是带着这个bug继续运行,这真的是你的选择。

希望这对你有所帮助!

从语法的角度来看没有问题,但是表达式5!=i背后的逻辑并不健全。

在我看来,使用!=来设置for循环的边界在逻辑上是不合理的,因为for循环会增加或减少迭代索引,因此将循环设置为迭代直到迭代索引超出边界(!= to something)不是一个正确的实现。

它可以工作,但它很容易发生错误行为,因为在使用!=来解决增量问题时(意味着你从一开始就知道它是增加还是减少),边界数据处理丢失了,这就是为什么使用!=而不是<>>==>

循环条件是一个强制循环不变量。

假设你不看循环体:

for (int i = 0; i != 5; ++i)
{
// ?
}

在这种情况下,你知道在循环迭代开始时i不等于5

for (int i = 0; i < 5; ++i)
{
// ?
}

在这种情况下,你知道在循环迭代开始时i小于5

第二个比第一个信息要多得多,不是吗?现在,程序员的意图(几乎可以肯定)是相同的,但如果您正在寻找错误,从阅读一行代码中获得信心是一件好事。第二个执行是不变量,这意味着在第一种情况下会咬你的一些bug在第二种情况下不会发生(或者不会导致内存损坏,比如说)。

<比用!=读更少的代码,你可以更多地了解程序的状态。而在现代cpu上,它们所花费的时间是一样的。

如果你的i没有在循环体中被操作,而且总是加1,而且开始时小于5,就不会有任何区别。但为了知道它是否被操纵,你必须确认这些事实。

其中一些事实相对简单,但你也可能会出错。然而,检查整个循环体是一件痛苦的事情。

在c++中,你可以这样写indexes类型:

for( const int i : indexes(0, 5) )
{
// ?
}

做与上面两个for循环中的任何一个相同的事情,甚至编译器优化它直到相同的代码。然而,在这里,你知道 i不能在循环体中被操作,因为它被声明为const,而代码不会破坏内存。

在不理解上下文的情况下,从一行代码中获得的信息越多,就越容易找到哪里出了问题。在整数循环的情况下,<!=提供了更多关于该行代码状态的信息。

除了这些例子之外,循环变量会(无意地)在语句体内部发生变化,还有其他使用小于或大于操作符的原因:

  • 否定使代码更难理解
  • <>只有一个字符,而!=有两个

在这种情况下使用关系比较是一种流行的习惯。在迭代器类别及其可比性等概念性考虑不被视为高优先级的时代,它获得了流行。

我想说的是,只要有可能,人们应该更喜欢使用相等比较而不是关系比较,因为相等比较对所比较的值的要求更少。EqualityComparable的要求比LessThanComparable的要求小。

另一个证明相等比较在这种上下文中更广泛适用性的例子是实现unsigned迭代到0的流行难题。可以这样做

for (unsigned i = 42; i != -1; --i)
...

请注意,上述方法同样适用于有符号迭代和无符号迭代,而关系版本则不适用无符号类型。

遵循这种做法有两个相关的原因,它们都与这样一个事实有关,即编程语言毕竟是一种将由人类(以及其他人)阅读的语言。

(1)有点冗余。在自然语言中,我们通常提供比严格必要的更多信息,就像纠错码一样。这里额外的信息是循环变量i(看到我在这里如何使用冗余了吗?如果你不知道“循环变量”是什么意思,或者如果你忘记了变量的名字,在阅读“循环变量i”之后,你有了完整的信息)在循环过程中小于5,而不仅仅是不同于5。冗余增强了可读性。

(2)约定。语言有表达特定情境的特定标准方式。如果你不遵循既定的表达方式,你仍然会被理解,但你的信息的接收者需要付出更大的努力,因为某些优化不会起作用。例子:

别围着热糊糊说话。只是照亮了困难!

第一句是一个德语成语的直译。第二种是常见的英语成语,主要词被同义词取代。结果是可以理解的,但要理解比下面的要长得多:

不要拐弯抹角。解释一下问题!

即使在第一个版本中使用的同义词碰巧比英语习语中的常规单词更适合这种情况,情况也是如此。类似的力量在程序员阅读代码时也会起作用。这也是为什么5 != i5 > i是奇怪的方式来放置它的原因。除非你所工作的环境是标准的,以这种方式交换更正常的i != 5i < 5。这样的方言社区确实存在,可能是因为一致性使得更容易记住写5 == i而不是自然但容易出错的i == 5

不使用这种结构的一个原因是浮点数。!=是一个非常危险的与浮点数的比较,因为即使数字看起来相同,它也很少求值为true。<>可以消除这种风险。

是的,OpenMP不会用!=条件并行化循环。

正如Ian Newson已经说过的,你不能可靠地循环浮动变量并使用!=退出。例如,

for (double x=0; x!=1; x+=0.1) {}

实际上将永远循环,因为0.1不能精确地用浮点数表示,因此计数器略低于1。使用<终止。

(但是请注意,不管你得到0.9999…作为最后接受的数字–这违反了小于假设;或者已经在1.0000000000000001退出。)