为什么 16 位和 32 位整数的有符号和无符号加法转换不同?

Why is signed and unsigned addition converted differently for 16 and 32 bit integers?

GCC 和 Clang 似乎对有符号整数和无符号整数之间的加法有不同的解释,具体取决于它们的大小。为什么会这样,转换在所有编译器和平台上是否一致?

举个例子:

#include <cstdint>
#include <iostream>

int main()
{

    std::cout <<"16 bit uint 2 - int 3 = "<<uint16_t(2)+int16_t(-3)<<std::endl;
    std::cout <<"32 bit uint 2 - int 3 = "<<uint32_t(2)+int32_t(-3)<<std::endl;
    return 0;
}

结果:

$ ./out.exe   
16 bit uint 2 - int 3 = -1
32 bit uint 2 - int 3 = 4294967295

在这两种情况下,我们都得到了 -1,但有一个被解释为无符号整数并下溢。我本来希望两者都以相同的方式转换。

所以,为什么编译器对它们的转换如此不同,这能保证一致吗?我用 g++ 11.1.0,clang 12.0 测试了这个。和 g++ 11.2.0 在 Arch Linux 和 Debian 上,得到相同的结果。

16 位 unsigned int 可以提升为 32 位 int 而不会由于范围差异而丢失任何值,所以就是这样。 32 位整数不是这样。

当您执行 uint16_t(2)+int16_t(-3) 时,两个操作数都是小于 int 的类型。因此,每个操作数都被提升为 int 并且 signed + signed 导致有符号整数,并且您得到 -1 的结果存储在该有符号整数中。

当您执行 uint32_t(2)+int32_t(-3) 时,由于两个操作数的大小都是 int 或更大,因此不会发生提升,现在您处于 unsigned + signed 的情况在将有符号整数转换为无符号整数时,-1unsigned 值换行为可表示的最大值。

So again, why do the compilers convert these so differently,

[language-lawyer] 的标准报价:

[expr.arith.conv]

Many binary operators that expect operands of arithmetic or enumeration type cause conversions and yield result types in a similar way. The purpose is to yield a common type, which is also the type of the result. This pattern is called the usual arithmetic conversions, which are defined as follows:

  • ...
  • Otherwise, the integral promotions ([conv.prom]) shall be performed on both operands. Then the following rules shall be applied to the promoted operands:
    • If both operands have the same type, no further conversion is needed.
    • Otherwise, if both operands have signed integer types or both have unsigned integer types, the operand with the type of lesser integer conversion rank shall be converted to the type of the operand with greater rank.
    • Otherwise, if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to the type of the operand with unsigned integer type.
    • Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, the operand with unsigned integer type shall be converted to the type of the operand with signed integer type.
    • Otherwise, both operands shall be converted to the unsigned integer type corresponding to the type of the operand with signed integer type.

[conv.prom]

A prvalue of an integer type other than bool, char8_­t, char16_­t, char32_­t, or wchar_­t whose integer conversion rank ([conv.rank]) is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

These conversions are called integral promotions.

std::uint16_t 类型 可能 具有比 int 更低的转换级别,在这种情况下,当用作操作数时它将被提升。 int 可能 能够表示 std::uint16_t 的所有值,在这种情况下,晋升将是 int。两个int的常见类型是int.

std::uint32_t 类型 可能 具有与 int 相同或更高的转化等级,在这种情况下,它不会被提升。无符号类型和同等级有符号类型的公共类型是无符号类型。


有关选择此转换行为的解释,请参阅“6.3.1.1 布尔值、字符和整数”一章 理由 国际标准- 编程语言- C"。我不会在这里引用整个章节。


is this guaranteed to be consistent?

一致性取决于实现定义的整数类型的相对大小。

Why is this,

C(以及 C++)有一条规则有效地说明当在表达式中使用小于 int 的类型时,它首先被提升为 int(实际规则比允许多个不同的规则稍微复杂一点大小相同的类型)。

Rationale for International Standard Programming Languages C 的第 6.3.1.1 节声称在早期的 C 编译器中有两个版本的提升规则。 “unsigned preserving”和“value preserving”并讨论了他们为什么选择“value preserving”选项。总而言之,他们认为它会在更大比例的情况下产生正确的结果。

然而,它并没有解释为什么促销的概念首先存在。我推测它的存在是因为在许多处理器上,包括最初设计 C 的 PDP-11,算术运算只对字进行运算,而不是对小于字的单位进行算术运算。因此,在表达式的开头将小于单词的所有内容转换为单词会更简单、更有效。

在今天的大多数平台上,int 是 32 位的。所以 uint16_t 和 int16_t 都被提升为 int。算法继续生成值为 -1 的 int 类型的结果。

OTOH uint32_t 和 int32_t 不小于 int,因此它们通过提升步骤保留其原始大小和签名。算术运算符的操作数何时为不同类型的规则开始起作用,并且由于操作数的大小相同,因此有符号操作数将转换为无符号操作数。

理似乎没有讲这个规则,这表明它可以追溯到pre-standard C.

and is the conversion consistent on all compilers and platforms?

在 Ansi C 或 ISO C++ 平台上,它取决于 int 的大小。对于 16 位 int,这两个示例都会给出较大的正值。使用 64 位 int 两个例子都会给出 -1.

在 pre-standard 实现中,两个表达式都可能 return 大正数。