glibc 'div()' 代码中的错误

Bug in glibc `div()` code?

本文关键字:错误 代码 div glibc      更新时间:2023-10-16

如"整数除法四舍五入与负数在C++"中引用,在 C 中,在 C99 之前(即在 C89 中(和在 C++ 中在 C++11 之前(即在 C++98 和 C++03 中(,对于任一操作数为负数的整数除法计算,余数的符号(或等效地,商的舍入方向(是实现定义的

然后是标准函数std::div,它被指定为将商截断为零(即余数与除数(分子(具有相同的符号((例如,请参阅"div(( 函数函数的目的是什么?"的答案(。

以下是glibc的div()代码(来源((也引用于"div函数有用吗(stdlib.h(?"(:

(注:div_t定义为:

typedef struct
  {
    int quot;
    int rem;
  } div_t;

-- 尾注。

/* Return the `div_t' representation of NUMER over DENOM.  */
div_t
div (numer, denom)
     int numer, denom;
{
  div_t result;
  result.quot = numer / denom;
  result.rem = numer % denom;
  /* The ANSI standard says that |QUOT| <= |NUMER / DENOM|, where
     NUMER / DENOM is to be computed in infinite precision.  In
     other words, we should always truncate the quotient towards
     zero, never -infinity.  Machine division and remainer may
     work either way when one or both of NUMER or DENOM is
     negative.  If only one is negative and QUOT has been
     truncated towards -infinity, REM will have the same sign as
     DENOM and the opposite sign of NUMER; if both are negative
     and QUOT has been truncated towards -infinity, REM will be
     positive (will have the opposite sign of NUMER).  These are
     considered `wrong'.  If both are NUM and DENOM are positive,
     RESULT will always be positive.  This all boils down to: if
     NUMER >= 0, but REM < 0, we got the wrong answer.  In that
     case, to get the right answer, add 1 to QUOT and subtract
     DENOM from REM.  */
  if (numer >= 0 && result.rem < 0)
    {
      ++result.quot;
      result.rem -= denom;
    }
  return result;
}

如您所见,在大注释块之后有一个测试,其目的是在内置除法截断为 -无穷大而不是朝零截断时"纠正"结果。

现在的问题:

该代码中没有错误吗?

我们先来看一下调用div(42, -5)的例子。"在数学中">42/-5 正好是 -8.4,因此理论上在 C89 和 C++03 中,42 / -5可能会产生-8(截断(或-9(地板(,具体取决于实现。读取代码:

  • 如果42 / -5产生-842 % -5产生2(如42 == -8 * -5 + 2(,因此测试(42 >= 0 && 2 < 0)这是不正确的,并且上述函数根据需要返回-82;
  • 如果42 / -5产生-942 % -5产生-3(如42 == -9 * -5 + -3(,因此测试(42 >= 0 && -3 < 0)这是真的,因此上面的函数返回"校正"的-9 + 1-3 - -5,即 -82,如愿以偿。

但是现在让我们考虑一下呼叫div(-42, 5)(符号倒置(:

  • 如果-42 / 5产生-8-42 % 5产生-2(如-42 == -8 * 5 + -2(,因此测试(-42 >= 0 && -2 < 0)不成立,并且上述函数根据需要返回-8-2;
  • 如果-42 / 5产生-9-42 % 5产生3(如-42 == -9 * 5 + 3(,因此测试(-42 >= 0 && 3 < 0)...不是真的!上面的函数返回 -93 而不是 -8-2

上面代码中的注释首先似乎是正确的,因为它说需要更正的情况是"REM 具有与 NUMBER 相反的符号",但随后它进行了巨大的简化"这一切都归结为:如果 NUMER>= 0,但 REM <0,我们得到了错误的答案",这对我来说似乎是错误的(不完整(,因为它省略了"如果 NUMER <0, 但 REM> 0"(在上一个示例中-423(。

我几乎不敢相信这样的错误自 1992 年或 1990 年以来一直没有被注意到(显然有人试图"修复"它,但它似乎仍然不正确,因为div(-42, 5)可以返回-108(......可以说,大多数实现默认都已截断为零(并且所有实现都需要从 C99 和 C++11 开始这样做,因此该问题在最新的标准 1 中"没有意义"(,因此错误不会出现在它们身上,但仍然......也许我在这里错过了什么?

感谢您的任何见解。


1(编辑(至于"这个问题在C++11和C99(及更新版本(中没有实际意义":因此,在这些标准中,内置除法需要截断为零,所以我们永远不需要调整结果,但这是否意味着目前的实现比需要的更复杂并且不必要的低效?"大评论"已经过时,if测试也没用,那么这部分不应该完全删除吗?

作为代码的原始作者,我不得不说:你是对的。 它坏了。 我们没有系统在测试中表现"错误",我可能在当天写了太晚(或早......(或其他什么。

我们被更新的标准所拯救,整个事情应该被清理掉,如有必要,也许可以用一小#ifdef(和更正的调整代码(来代替 C99 之前。

(我还要注意的是,原版没有用 GNU 风格的缩进缩进:-(

有理由,为什么它不能被实现定义以及它必须是什么。

TLDR:余数符号必须与除法符号匹配。

理由:
当恢复除以值时,知道除法结果、余数和除数,它的余数必须与除数除积相加。如果 remander 标志是特定于实现的,那么总和的操作也应该是特定于实现的,不是吗?或者至少在除法后有一个代码,如果它不正确,它将修复余数符号。