【问题标题】:Is there a technical reason to use > (<) instead of != when incrementing by 1 in a 'for' loop?在“for”循环中以 1 递增时,是否有使用 > (<) 而不是 != 的技术原因?
【发布时间】:2015-10-05 23:35:16
【问题描述】:

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

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

for 循环中递增 1 时,是否有技术原因使用 &gt;&lt; 而不是 !=?或者这更像是一种约定?

【问题讨论】:

  • 它使它更具可读性,此外,如果您将i++ 更改为i+=2(例如),它将运行很长时间(或可能永远)。现在,由于您通常将 &lt; 用于将迭代器递增 超过 1 的情况,因此您也可以将 &lt; 用于将其递增 1 的情况(为了保持一致性)。
  • 5 != i 是邪恶的
  • 你应该时刻注意自己在做什么,但无论如何你都会犯错。使用
  • @OleTange 如果您使用浮点数进行迭代,您已经搞砸了..
  • @Slava...不是邪恶的。你显然从来没有被 C 语言中的一个简单的拼写错误所困扰,这使得if ( i == 5 ) ...if ( i = 5 ) 之间有所不同。非常不同的东西。反转操作数可以防止出现问题:由于文字不是lvals,因此无法将它们分配给并且编译器会出现拼写错误。 @Zingam:良好的防御性编程!

标签: c++ c for-loop


【解决方案1】:

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

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

那么对于ab 的任何值,它都会按预期工作(即,当a &gt; b 时零迭代,而不是如果您使用i == b; 则无限迭代)。

另一方面,对于您要编写的迭代器

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

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

也是基于范围的for循环

for (auto e : v)

不仅仅是花哨的糖,而且它们可以显着减少编写错误代码的机会。

【讨论】:

  • 很好的例子。我会对!= 而不是&lt; 的案例感兴趣。我个人从未使用过我认为的。
  • @Elyasin 我没有找到一个好的,我不想发布一个坏的:想象一下你有某种圆形容器,你在容器 N 的大小模上增加 +x 和当你达到某个索引时,你想停止循环......这是一个非常糟糕的例子。也许我会找到一个更好的
  • 使用 != 而不是 )在这种情况下的优势是什么。 for (int i=a; i != b; i++) {}
  • != 之外的任何东西与迭代器一起使用是非常危险的,因为 有时 迭代器可以小于比较(例如,它是一个指针)但比较毫无意义(它是一个链表)。
  • 认真学习使用空白。
【解决方案2】:

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

为此,答案是:否(*)。 (*) 是“请查阅您的处理器手册”。如果您正在使用一些边缘案例 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;
    }
}

还有一些对&lt; 的约定偏见,因为标准容器中的排序通常依赖于operator&lt;,例如,多个 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

实现者必须提供两个比较函数。于是&lt;成为了受青睐的运营商。

【讨论】:

  • 为了便于阅读,我 += 步骤应该在控制部分。在 if you would step = 0
  • 虽然这有助于良好代码的可读性,但我想强调错误版本中的缺陷
【解决方案3】:

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

有重复的问题here;还有一个有趣的answer

【讨论】:

  • 有些类型,比如迭代器,不支持,但支持==和!=。
  • 这是一个带有 int 的 for 循环。没有迭代器。
  • “但是可以降低风险、可维护性和更好地理解代码。” 所有这些都是技术原因。 :-)
  • @T.J.Crowder 当您只想谈论机器会做什么时,这是什么原因?
  • @DanSheppard 我通常认为降低风险和可维护性是商业原因,更好地理解代码是社会原因(尽管在这种情况下这些原因都适用于技术问题) . “降低风险”的考虑绝不限于技术问题,如果您发现自己与不同的人一起工作,“更好地理解代码”的考虑可能会改变(即使所涉及的机器根本没有改变) )。
【解决方案4】:

变量i 可能设置为某个较大的值,如果您只使用!= 运算符,您将最终陷入无限循环。

【讨论】:

  • 并不是无穷无尽的——在 C 的大多数实现中,i 会在最大时默默地环绕。但是,是的,很可能比您准备等待的时间更长。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

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

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

    它会起作用,但它很容易出现错误行为,因为在使用 != 处理增量问题时会丢失边界数据处理(这意味着您从一开始就知道它是递增还是递减),这就是为什么而不是 @987654325 @&lt;&gt;&gt;==&gt; 都使用了。

    【讨论】:

    • 逻辑非常好。当i5 时,循环退出。你是不是想说点别的?
    • 如果数据由于热量而无法正确传输,电磁会影响您的代码永远运行。如果您在具有 ECC RAM 的常规工作站或服务器上执行此操作,则没有问题(逻辑、技术或物理问题)
    【解决方案7】:

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

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

    (2) 约定。语言有特定的标准方式来表达某些情况。如果您不遵循既定的表达方式,您仍然会被理解,但您的消息接收者的努力会更大,因为某些优化不会起作用。示例:

    不要谈论热土豆泥。只是照亮困难!

    第一句是德语成语的直译。二是用同义词代替主要词的常见英语习语。结果是可以理解的,但比这要花更长的时间才能理解:

    不要拐弯抹角。把问题解释清楚!

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

    【讨论】:

    • Ausgezeichnet。类似地,不会将测试写成i &lt; 17+(36/-3)(即使这些是其他地方使用的常量;为了清楚起见,您必须写出它们的名称!)。
    【解决方案8】:
    while (time != 6:30pm) {
        Work();
    }
    

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

    这表明更强的限制可以降低风险,并且可能更易于理解。

    【讨论】:

    • 有时,试图“降低风险”最终只是隐藏了错误。如果环路的设计应该排除下午 6:30避免被忽视,然后使用&lt; 将隐藏错误,但使用!= 会明显表明存在问题。
    • @AdrianMcCarthy:不一定;它可能只会引入第二个错误,使诊断变得更难
    • @AdrianMcCarthy:这就是我的观点;你可能有一些你还没有见过的实例;)
    • 迭代器是我认为@AdrianMcCarthy 观点的完美例子。推荐的比较是 != 而不是 <.>
    • 我已经看到了这个错误(它导致了 3 个月的调试)——但这个答案完全没有抓住重点。在给定的示例中,不可能错过结束条件。你甚至可以说有意识地选择 != 而不是
    【解决方案9】:

    正如 Ian Newson 所说,您不能可靠地循环一个浮动变量并使用!= 退出。例如,

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

    实际上会永远循环,因为 0.1 不能精确地用浮点数表示,因此计数器差点错过 1。&lt; 它会终止。

    (但请注意,无论您获得 0.9999... 作为最后一个接受的数字(哪种违反小于假设)或已经在 1.0000000000000001 退出,这基本上都是未定义的行为。)

    【讨论】:

      【解决方案10】:

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

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

      【讨论】:

      • 支持这一点的一个很好的例子:内存容易受到辐射引起的位翻转 (SEUs) 的影响。当将int 用于i != 15 条件时,如果翻转第四位以上的任何内容,计数器将运行很长时间,尤其是在sizeof(int) 为64 的机器上。SEU 在高海拔地区是一个非常现实的问题或者在太空中(因为更高的辐射),或者在大型超级计算机中(因为存在如此多的内存)。
      • 认为当辐射改变你的记忆时你的程序会运行正常是一种乐观的方式和抗灾难性的思维方式......好评论! :)
      【解决方案11】:

      不使用此构造的一个原因是浮点数。 != 与浮点数一起使用是一个非常危险的比较,因为即使数字看起来相同,它也很少评估为真。 &lt;&gt; 消除了这种风险。

      【讨论】:

      • 如果你的循环迭代器是一个浮点数,那么 !=&lt; 是最少的问题。
      【解决方案12】:

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

      假设你不看循环体:

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

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

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

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

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

      使用&lt; 比使用!= 阅读更少的代码,您可以更了解程序的状态。而在现代 CPU 上,它们所花费的时间相同。

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

      其中一些事实相对容易,但您可能会出错。然而,检查整个循环体是一件很痛苦的事情。

      在 C++ 中,您可以编写 indexes 类型,这样:

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

      与上述两个for 循环中的任何一个执行相同的操作,甚至是编译器将其优化为相同的代码。但是,您知道不能在循环体中操作i,因为它被声明为const,而代码不会破坏内存。

      无需了解上下文即可从一行代码中获得的信息越多,就越容易找出问题所在。在整数循环的情况下,&lt;!= 为您提供有关该行代码状态的更多信息。

      【讨论】:

        【解决方案13】:

        在这种情况下使用关系比较是一种流行的习惯,而不是其他任何东西。在迭代器类别及其可比性等概念性考虑不被视为高优先级的时代,它重新流行起来。

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

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

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

        请注意,上述内容同样适用于有符号和无符号迭代,而关系版本则分解为无符号类型。

        【讨论】:

          【解决方案14】:

          除了循环变量将(无意)在主体内更改的示例之外,还有其他原因需要使用小于或大于运算符:

          • 否定使代码更难理解
          • &lt;&gt; 只是一个字符,但 != 两个

          【讨论】:

          • 第二个子弹充其量只是一个无聊的理由。代码应该是正确和清晰的。紧凑远没有那么重要。
          • @JonathanLeffler 好吧,在 TDD REPL 中,额外的字符是一纳秒的解析时间,需要十亿次迭代才能找到由编写 5 != $i 引起的错误,这只需要一个整体我生命中的第二次。呃……窃笑
          【解决方案15】:

          使用&lt; 的最常见原因是约定俗成。更多的程序员认为这样的循环是“当索引在范围内时”而不是“直到索引到达末尾”。尽可能遵守约定是有价值的。

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

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

          例如,如果您正在单步执行纯 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 循环、标准算法或迭代器,即使长度很容易获得。如果您使用迭代器,约定是使用!= 而不是&lt;,如下所示:

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

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

          【讨论】:

          • 您在编程中强调习语的重要性是非常正确的。如果我看一些代码,它有一个熟悉的模式,我不需要浪费时间推断模式,而是可以直接进入它的核心。我想这是我对尤达比较的主要看法——它们看起来并不惯用,所以我最终不得不阅读它们两次以确保它们的意思是我认为的意思。
          【解决方案16】:

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

          在这种情况下,人们更喜欢 > 和

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

          如果我们使用 (i

          您是否希望程序中出现错误以关闭整个程序或继续处理那里的错误,这真的是您的选择。

          希望这有帮助!

          【讨论】:

          • 一个人可能会认为无限循环是错误的一个很好的指标,但另一个人会认为 i=5 不一定是错误
          【解决方案17】:

          我认为这样的表达方式

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

          更能表达意图
          for ( int i = 0 ; i != 100 ; ++i )
          {
            ...
          }
          

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

          【讨论】:

          • “我希望这个循环最多运行 5 次”与“我想根据需要运行这个循环多次,但正好 5 次除外,这是我不想要的。”跨度>
          • +1 用于提及范围的上限。我认为这对于数值范围很重要。 != 没有在这样的 for 循环中定义适当的范围
          【解决方案18】:

          当您最常使用!= 表示法时,迭代器是一个重要的例子:

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

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

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

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

          【讨论】:

          • 迭代器通常只有相等/不等的概念,而不是排序,这就是你使用!= 的原因。例如,在std::vector 中,您可以使用&lt;,因为迭代器与索引/指针相关联,但在std::set 中没有如此严格的排序,因为它遍历二进制树。
          【解决方案19】:

          正如您从其他众多答案中看到的那样,有理由使用

          但老实说,我认为你不能足够强调惯例的重要性。对于这个例子,其他程序员很容易看到你在做什么,但它会导致重复。编程时的其中一项工作是让每个人都尽可能地阅读和熟悉它,因此当有人必须更新/更改您的代码时,不可避免地要弄清楚您在不同的代码块中做了什么.如果我看到有人使用!=,我会假设他们使用它而不是&lt; 是有原因的,如果它是一个大循环,我会查看整个事情,试图弄清楚你做了什么必要的……那是浪费时间。

          【讨论】:

            【解决方案20】:

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

            【讨论】:

            • 您可能对重载的数量是正确的,但是在考虑对对象的要求时,几乎可以为任何对象定义一个!= 运算符,但并不总是可以定义一个严格的弱排序。从这个意义上讲,!= 会更好,因为它对类型的要求更少。不过,我还是留在&lt;
            【解决方案21】:

            你可以有类似的东西

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

            如果您的循环变量是由内部代码编写的,i!=5 可能不会中断该循环。这样检查不等式会更安全。

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

            【讨论】:

            • 这种内在的“if(condition) then i++”是我最先想到的。
            • 期待这是投票最高的答案;我很惊讶我不得不滚动这么远才能找到它。这比“好吧,你应该使用最强的条件”更能说服新手程序员。其他答案,如果它们保持在首位,则应将其作为对 OP 提议的代码可能出现问题的清晰而明显的解释。
            • @msouth 我完全同意,但我的 POV 很主​​观;)
            • @msouth 当意外的生产行为暴露我的错误时,我喜欢它,不是吗? :-)
            • @msouth 看,我们所要做的就是永远不要犯任何错误,一切都会好起来的。简单。
            猜你喜欢
            • 2010-12-19
            • 2014-04-20
            • 2010-09-15
            • 1970-01-01
            • 2012-03-05
            • 1970-01-01
            • 1970-01-01
            • 2017-04-03
            • 1970-01-01
            相关资源
            最近更新 更多