【问题标题】:Idiom for handling size_t underflow in loop condition在循环条件下处理 size_t 下溢的成语
【发布时间】:2020-02-15 07:30:17
【问题描述】:

在 C 和 C++ 中,size_t 是用于表示大小的无符号类型。它表达了意图并在某种程度上简化了范围断言(len < upper_bound vs len >= 0 && len < upper_bound 用于有符号整数)。

(在下面的所有示例中,len 表示数组的长度a)。

for 循环的习惯用法是:for (i = 0; i < len; i++)。向后 for 循环的习惯用法是 for (i = len-1; i >= 0; i--)。但是无符号循环索引会引入一些细微的错误,而且我经常会弄乱边缘情况。

首先,反向 for 循环。此代码在 len=0 时下溢。

for (size_t i = len-1; i >= 0; i--) { // Bad: Underflows for len=0
    use(a[i]);
}

这里有个-->“操作员”的把戏,如果你不习惯的话,看起来很奇怪。

for (size_t i = len; i--> 0;) {
    use(a[i]);
}

您可以对循环索引变量使用有符号类型,但如果len > INT_MAX 则会溢出。许多人和组织认为这种风险非常小,所以他们只坚持int

for (int i = len-1; i >= 0; i--) {  // BAD: overflows for len < INT_MAX
    use(a[i]);
}

所以我决定采用这种结构,因为它最接近典型的 for 循环形式并且具有最简单的表达式。

for (size_t i = len; i > 0; i--) {
    size_t pos = i-1;
    use(a[pos]);
}

我的问题从 0 迭代到 len-1

也就是说,在范围 [0, len-1) 上循环。当len=0 时,此循环下溢。

for (size_t i = 0; i < len-1; i++) {   // BAD: Underflows for len=0.
    use(a[i]);
}

对于向后迭代的情况,你可以使用有符号整数,但这可能会导致溢出。

for (int i = 0; i < len-1; i++) {    // BAD: Will overflow if len > INT_MAX
    use(a[i]);
}

我倾向于在循环条件中添加另一个表达式,检查len &gt; 0,但这感觉很笨拙。

for (size_t i = 0; len > 0 && i < len-1; i++) {
    use(a[i]);
}

我可以在循环之前添加一个 if 语句,但这也感觉很笨拙。

有没有更简单的方法来编写一个无符号索引变量从 0 循环到 len-1 的 for 循环?

【问题讨论】:

  • 为什么不在循环前检查 len > 0 和其他边界?
  • 关于--&gt; 操作符,中间加一个空格,就不会那么奇怪了。
  • 我愿意for (size_t i = len-1; i != (size_t)-1; i--)
  • 请注意for (size_t i = len-1; i &gt;= 0; i--) 是一个无限循环。
  • 即使len &gt; 0,您的第一个示例也是错误的。问题是i &gt;= 0 总是正确的。

标签: c++ c for-loop unsigned size-t


【解决方案1】:

这里有两种情况。

0 向前迭代到len - 2(包括)

for (size_t i = 0; i + 1 < len; ++i) {
    size_t index = i;
    // use index here
}

len - 2 向后迭代到 0 包括

for (size_t i = len; i > 1; --i) {
    size_t index = i - 2;
    // use index here
}

【讨论】:

    【解决方案2】:

    怎么样

    for (size_t i = 0; i+1 < len; i++) {
        use(a[i]);
    }
    

    【讨论】:

      【解决方案3】:

      在所有这些情况下,您都有一个共同的主题:您有一个具有起始值的索引。它会递增或递减,直到达到结束值,从而终止循环。

      这里有一个帮助是明确这两个值。在最简单的情况下,向前迭代整个范围,最终值就是len

      for (size_t i=0, end=len; i!=end; ++i) {
          ///...
      }
      

      这遵循给使用迭代器的人的一般建议。特别要注意比较,它在这里有效,实际上是某些迭代器所需要的。

      现在,向后迭代:

      for (size_t i=len-1, end=-1; i!=end; --i) {
          ///...
      }
      

      最后,向后迭代一个子集,不包括该范围的最后一个 n 元素:

      if (len > n) {
          for (size_t i=len-n-1, end=-1; i!=end; --i) {
              ///...
          }
      }
      

      实际上,您正在与之抗争的是您试图将太多东西放入循环逻辑中。请明确指出,这需要超过 n 元素才能执行任何操作。是的,您可以将len &gt; n 放入循环条件中,但这不会为您提供任何人都能理解的清晰简单的代码。

      【讨论】:

        【解决方案4】:

        我倾向于在循环条件中添加另一个表达式,检查 len > 0,但感觉很笨拙。

        您的代码应遵循逻辑。它一点也不笨拙。而且它对人类来说更具可读性。

        如果len == 0 和我通常使用if 语句,则循环毫无意义。它使代码易于理解和维护。

        if(len)
        {
            for (size_t i = 0; i < len-1; i++) { /*...*/ }       
        }
        

        或者您也可以在循环中添加检查。顺便说一句,您只检查它是否不为零。

         for (size_t i = 0; len && i < len-1; i++) { /*...*/ }       
        

        【讨论】:

          【解决方案5】:

          这个问题有很多可能的答案。哪个最好是基于意见的。我会提供一些选择。

          为了以相反的顺序遍历数组的所有元素,使用无符号索引,一个选项是

          for (size_t index = 0; index < len; ++index)
          {
               size_t i = len - 1 - index;
               use(a[i]);
          }
          

          或(更简单)

          for (size_t i = 0; i < len; ++i)
          {
               use(a[len - 1 - i]);
          }
          

          在这两种情况下,如果len 为零,则不会执行循环体。尽管循环是递增而不是递减,但两者都以相反的顺序访问元素。如果你经常写这样的循环,写一点形式的内联函数也不难

          size_t index_in_reverse(size_t index, size_t len)
          {
               return len - 1 - index;
          }
          

          然后做

          for (size_t i = 0; i < len; ++i)
          {
               use(index_in_reverse(i, len));
          }
          

          要以前向顺序迭代循环的所有元素,除了最后一个,我会明确说明,而不是尝试在循环条件下进行。

          if (len > 0)
          {
               size_t  shortened_len = len - 1;
               for (size_t i = 0; i < shortened_len; ++i)
                   use(a[i]);
          }
          

          我引入变量shortened_len 的原因是为了让代码自我记录它没有迭代整个数组的事实。我见过太多的情况,其中i &lt; len - 1 形式的条件被后续开发人员“更正”以删除- 1,因为他们认为这是一个错字。

          这可能会让 OP 觉得“笨拙”,但我建议

          for (size_t i = 0; len > 0 && i < len-1; i++) {
             use(a[i]);
          } 
          

          对于人类来说更难理解(将多个测试放在一个循环条件中,迫使维护代码的人实际计算出这两个条件的作用以及它们如何交互)。如果要在“简洁”的代码或“不太简洁但消耗不熟悉的人的脑力来理解”的代码之间进行选择,我将始终选择后者。请记住,六个月后维护代码的用户可能是您自己,没有什么比“哪个白痴写这个??哦,是我!”更令人谦卑的思考过程。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-11-14
            • 1970-01-01
            • 1970-01-01
            • 2012-06-24
            • 2016-01-05
            • 2017-07-31
            • 2021-11-25
            • 2011-10-04
            相关资源
            最近更新 更多