无符号整数减法是不是定义了行为?

Posted

技术标签:

【中文标题】无符号整数减法是不是定义了行为?【英文标题】:Is unsigned integer subtraction defined behavior?无符号整数减法是否定义了行为? 【发布时间】:2011-11-05 11:46:34 【问题描述】:

我遇到了一些人的代码,他似乎认为从另一个相同类型的整数中减去一个无符号整数会出现问题,而结果是负数。因此,即使它碰巧适用于大多数架构,这样的代码也是不正确的。

unsigned int To, Tf;

To = getcounter();
while (1) 
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) 
        break;
     

这是我能从 C 标准中找到的唯一模糊相关的引用。

涉及无符号操作数的计算永远不会溢出,因为 结果不能用得到的无符号整数表示 type 以比最大数大一的数为模减少 可以由结果类型表示的值。

我想人们可以把这句话理解为当右操作数更大时,该操作被调整为在模截断数字的上下文中有意义。

0x0000 - 0x0001 == 0x 1 0000 - 0x0001 == 0xFFFF

而不是使用依赖于实现的签名语义:

0x0000 - 0x0001 == (无符号)(0 + -1) == (0xFFFF 但也有 0xFFFE 或 0x8001)

哪种或哪种解释是正确的?有定义吗?

【问题讨论】:

标准中的单词选择是不幸的。它“永远不会溢出”意味着它不是错误情况。使用标准中的术语,而不是溢出值“wraps”。 【参考方案1】:

当您使用 unsigned 类型时,modular arithmetic(也称为 “环绕” 行为)正在发生。要理解这个模数运算,只需看看这些时钟:

9 + 4 = 1 (13 mod 12),所以另一个方向是:1 - 4 = 9 (-3 mod 12)。处理无符号类型时应用相同的原则。如果结果类型unsigned,则进行模运算。


现在看看以下将结果存储为unsigned int 的操作:

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

当您想确保结果为signed 时,将其存储到signed 变量中或将其转换为signed。当你想得到数字之间的差异并确保不会应用模运算时,你应该考虑使用stdlib.h中定义的abs()函数:

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

要非常小心,尤其是在编写条件时,因为:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

但是

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...

【讨论】:

int d = abs(five - seven); 行不行。首先计算five - seven:提升将操作数类型保留为unsigned int,结果以(UINT_MAX+1) 为模计算,计算结果为UINT_MAX-1。那么这个值就是abs的实际参数,这是个坏消息。 abs(int) 导致传递参数的未定义行为,因为它不在范围内,abs(long long) 可能会保留该值,但是当返回值被强制为 int 以初始化 d 时,会发生未定义行为。跨度> int c = five - seven; 行更直接错误,原因相同。 @LihO:C++ 中唯一一个上下文相关且根据其结果的使用方式而有所不同的运算符是自定义转换运算符operator T()。我们正在讨论的两个表达式中的加法是在类型unsigned int 中执行的,基于操作数类型。添加的结果是unsigned int。然后该结果被隐式转换为上下文所需的类型,转换失败,因为该值在新类型中不可表示。 @LihO:想想double x = 2/3; vs double y = 2.0/3;可能会有所帮助 嗯,这可能不是未定义的行为,因为这是将超出范围的值转换为有符号整数类型,而不是在计算过程中溢出。所以它将是实现定义的。【参考方案2】:

在无符号类型中产生负数的减法结果是明确定义的:

    [...] 涉及无符号操作数的计算永远不会溢出, 因为不能用生成的无符号整数类型表示的结果是 以比最大值大一的数字为模减少,可以是 由结果类型表示。 (ISO/IEC 9899:1999 (E) §6.2.5/9)

如您所见,(unsigned)0 - (unsigned)1 等于 -1 模 UINT_MAX+1,或者换句话说,UINT_MAX。

请注意,虽然它确实说“涉及无符号操作数的计算永远不会溢出”,这可能会让您相信它仅适用于超过上限,但这是作为 动机句子的实际绑定部分:“无法由生成的无符号整数类型表示的结果是 以比最大值大一的数字为模减少,可以是 由结果类型表示。”这句话不限于溢出类型的上限,同样适用于太低而无法表示的值。

【讨论】:

谢谢!我现在看到了我所缺少的解释。我认为他们本可以选择更清晰的措辞。 我现在感觉好多了,知道如果任何无符号加法滚到零并导致混乱,那将是因为 uint 始终旨在表示整数 @ 的数学 ring 987654324@到UINT_MAX,加法和乘法取模UINT_MAX+1,不是因为溢出。然而,它确实提出了一个问题,如果环是一种基本数据类型,为什么该语言不提供对其他大小环的更一般支持。 @TheodoreMurdock 我认为这个问题的答案很简单。据我所知,它是一个戒指的事实是一个结果,而不是一个原因。真正的要求是无符号类型的所有位都必须参与值表示。环状行为自然而然地由此而来。如果您想要其他类型的此类行为,请执行算术运算,然后应用所需的模数;使用基本运算符。 @underscore_d 当然......很清楚他们做出设计决定的原因。有趣的是,他们将规范大致写为“没有算术上溢/下溢,因为数据类型被指定为环”,好像这种设计选择意味着程序员不必小心避免过度和不足-flow 或让他们的程序严重失败。【参考方案3】:

嗯,第一个解释是正确的。但是,您在这种情况下对“签名语义”的推理是错误的。

同样,您的第一个解释是正确的。无符号运算遵循模运算规则,这意味着对于 32 位无符号类型,0x0000 - 0x0001 的计算结果为 0xFFFF

然而,第二种解释(基于“签名语义”的解释)也需要产生相同的结果。 IE。即使您在有符号类型的域中评估0 - 1 并获得-1 作为中间结果,这个-1 仍然需要在稍后转换为无符号类型时产生0xFFFF。即使某些平台对有符号整数(1 的补码,有符号幅度)使用奇异的表示,在将有符号整数值转换为无符号整数值时,仍需要该平台应用模运算规则。

比如这个评价

signed int a = 0, b = 1;
unsigned int c = a - b;

仍然保证在c 中产生UINT_MAX,即使平台对有符号整数使用奇异的表示。

【讨论】:

我认为你的意思是 16 位无符号类型,而不是 32 位。【参考方案4】:

对于unsigned int 或更大类型的无符号数,在没有类型转换的情况下,a-b 被定义为产生无符号数,当添加到b 时,将产生a。将负数转换为无符号定义为产生的数字在添加到符号反转的原始数字时将产生零(因此将 -5 转换为无符号将产生一个值,当添加到 5 时将产生零) .

请注意,小于unsigned int 的无符号数可能会在减法之前提升为类型inta-b 的行为将取决于int 的大小。

【讨论】:

【参考方案5】:

嗯,无符号整数减法已经定义了行为,也是一件棘手的事情。当您减去两个无符号整数时,如果未明确指定结果(左值)类型,则结果将提升为更高类型的 int。在后一种情况下,例如 int8_t result = a - b; (其中 a 和 b 具有 int8_t 类型)您可以获得非常奇怪的行为。我的意思是你可能会丢失transitivity property(即如果 a > b 和 b > c,那么 a > c 是真的)。 传递性的丧失会破坏tree-type data structure 的工作。必须注意不要为排序、搜索、树构建提供比较功能,使用无符号整数减法来推断哪个键更高或更低。

请参阅下面的示例。

#include <stdint.h>
#include <stdio.h>

void main()

    uint8_t a = 255;
    uint8_t b = 100;
    uint8_t c = 150;

    printf("uint8_t a = %+d, b = %+d, c = %+d\n\n", a, b, c);

    printf("          b - a  = %+d\tpromotion to int type\n"
           " (int8_t)(b - a) = %+d\n\n"
           "          b + a  = %+d\tpromotion to int type\n"
           "(uint8_t)(b + a) = %+d\tmodular arithmetic\n"
           "     b + a %% %d = %+d\n\n", 
           b - a,  (int8_t)(b - a), 
           b + a, (uint8_t)(b + a),
           UINT8_MAX + 1,
           (b + a) % (UINT8_MAX + 1));

    printf("c %s b (b - c = %d), b %s a (b - a = %d), AND c %s a (c - a = %d)\n",
           (int8_t)(c - b) < 0 ? "<" : ">", (int8_t)(c - b),
           (int8_t)(b - a) < 0 ? "<" : ">", (int8_t)(b - a),
           (int8_t)(c - a) < 0 ? "<" : ">", (int8_t)(c - a));

$ ./a.out 
uint8_t a = +255, b = +100, c = +150

          b - a  = -155 promotion to int type
 (int8_t)(b - a) = +101

          b + a  = +355 promotion to int type
(uint8_t)(b + a) = +99  modular arithmetic
     b + a % 256 = +99

c > b (b - c = 50), b > a (b - a = 101), AND c < a (c - a = -105)

【讨论】:

【参考方案6】:
int d = abs(five - seven);  // d =  2

std::abs 不“适合”无符号整数。不过需要演员表。

【讨论】:

以上是关于无符号整数减法是不是定义了行为?的主要内容,如果未能解决你的问题,请参考以下文章

将负双精度转换为无符号整数的行为是不是在 C 标准中定义? ARM 与 x86 上的不同行为

无符号字符的减法运算

有符号和无符号之间的减法,然后是除法

Java不提供无符号整数类型?谢谢

在这个 C90 未定义的行为定义中,“有符号或无符号类型”是啥意思?

16位寄存器的SSE无符号/有符号减法