正如许多人指出的那样,在当前的 C 和 C++ 标准中,x % n
不再为x
和的任何值定义实现n
。x / n
在未定义 [1]的情况下,它是未定义的行为。此外,x - y
在整数溢出的情况下是未定义的行为,如果 和 的符号可能不同,这是可能x
的y
。
因此,通用解决方案的主要问题是避免整数溢出,无论是在除法还是减法中。如果我们知道x
andy
是非负的并且n
是正的,那么溢出和被零除是不可能的,我们可以自信地说这(x - y) % n
是定义的。不幸的是,x - y
可能是负数,在这种情况下,%
运算符的结果也是如此。
n
如果我们知道结果是肯定的,就很容易纠正结果是否定的;我们所要做的就是无条件地添加n
并执行另一个modulo
操作。这不太可能是最好的解决方案,除非你有一台除法比分支快的计算机。
如果有条件加载指令可用(这几天很常见),那么编译器可能会很好地处理以下代码,这些代码是可移植且定义明确的,受以下约束x,y ≥ 0 ∧ n > 0
:
((x - y) % n) + ((x >= y) ? 0 : n)
例如,gcc 为我的核心 I5 生成此代码(尽管它的通用性足以在任何非古生代英特尔芯片上工作):
idivq %rcx
cmpq %rsi, %rdi
movl $0, %eax
cmovge %rax, %rcx
leaq (%rdx,%rcx), %rax
这是愉快的无分支。(条件移动通常比分支快得多。)
另一种方法是(除了sign
需要编写函数):
((x - y) % n) + (sign(x - y) & (unsigned long)n)
如果sign
参数为负数,则全为 1,否则为 0。符号的一种可能实现(改编自bithacks)是
unsigned long sign(unsigned long x) {
return x >> (sizeof(long) * CHAR_BIT - 1);
}
这是可移植的(定义了将负整数值转换为无符号),但在缺乏高速移位的架构上可能会很慢。它不太可能比以前的解决方案更快,但是 YMMV。蒂亚斯。
对于可能整数溢出的一般情况,这些都不会产生正确的结果。处理整数溢出非常困难。(一个特别烦人的情况是n == -1
,尽管您可以对其进行测试并返回 0 而无需使用%
。)此外,您需要确定您对负模结果的偏好n
。我个人更喜欢定义x%n
为 0 或具有相同符号的定义n
- 否则你为什么要使用负除数 - 但应用程序不同。
n
如果不是-1
并且n + n
不会溢出,Tom Tanner 提出的三模解决方案将有效。n == -1
如果是x
or将失败,如果y
is ,使用而不是的INT_MIN
简单修复将失败。绝对值大的情况可以用比较代替,但是有很多极端情况,而且由于标准不需要2的补码算法,所以很难预测什么是极端情况。 [2]。abs(n)
n
n
INT_MIN
n
最后一点,一些诱人的解决方案不起作用。你不能只取 的绝对值(x - y)
:
(-z) % n == -(z % n) == n - (z % n) ≠ z % n
(除非z % n
碰巧是n / 2
)
而且,出于同样的原因,您不能只取模结果的绝对值。
此外,您不能只(x - y)
转换为无符号:
(unsigned)z == z + 2k (for some k) if z < 0
(z + 2k) % n == (z % n) + (2k % n) ≠ z % n
除非(2k % n) == 0
[1]x/n
并且x%n
如果n==0
. 但是x%n
如果是“不可表示”(即整数溢出)也是未定义x/n
的,这将发生在二进制补码机器(即您关心的所有机器)上,如果x
是最负的可表示数并且n == -1
. 很清楚为什么x/n
在这种情况下应该是 undefined ,但在 的情况下稍微少一些x%n
,因为该值是(数学上)0
。
[2] 大多数抱怨预测浮点运算结果困难的人并没有花太多时间尝试编写真正可移植的整数运算代码:)