为什么无符号短整型(乘以)无符号短整型会转换为有符号整型?

23

为什么在C++11中,unsigned short * unsigned short会转换成int

由于这行代码所示,int太小了,无法处理最大值。

cout << USHRT_MAX * USHRT_MAX << endl;

MinGW 4.9.2 上的溢出问题

-131071

因为 (来源):
USHRT_MAX = 65535 (2^16-1) 或更高*
INT_MAX = 32767 (2^15-1) 或更高*
而且 (2^16-1)*(2^16-1) = ~2^32。
这个解决方案会有什么问题吗?
unsigned u = static_cast<unsigned>(t*t);

这个程序
unsigned short t;
cout<<typeid(t).name()<<endl;
cout<<typeid(t*t).name()<<endl;

产生输出

t
i

on

gcc version 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC)
gcc version 4.8.2 (GCC)
MinGW 4.9.2

与两者皆有
g++ p.cpp
g++ -std=c++11 p.cpp

这证明在这些编译器上,t*t 被转换为 int

有用的资源:

C中的有符号到无符号转换 - 是否总是安全的?

有符号和无符号整数乘法

https://bytes.com/topic/c-sharp/answers/223883-multiplication-types-smaller-than-int-yields-int

http://www.cplusplus.com/reference/climits

http://en.cppreference.com/w/cpp/language/types


编辑:我已在以下图片中演示了问题。

enter image description here


1
如果在您的平台上,int 是16位,则您得到的结果不是一个 int。请注意,您链接到的表格中的值的免责声明:“实际值取决于特定系统和库实现,但应反映目标平台上这些类型的限制。” - Some programmer dude
1
你确定 USHRT_MAXunsigned short 类型吗?在我的环境下(Lubuntu 下的 GCC 4.8 64 位),USHRT_MAX 实际上是 int 类型(定义为 (32767 * 2 + 1))。难怪 USHRT_MAX*USHRT_MAX 会溢出。 - Paolo M
已确认在Mac OSX上适用于clang 3.5 - Walter
1
USHRT_MAX0xFFFF,而 0xFFFF * 0xFFFF = 0xFFFE0001(没有溢出)。这相当于 4294836225-131071,所以只是最终转换为 int 时才会出现问题。 - Barmak Shemirani
1
@BarmakShemirani INT_MAX0x7FFFFFFF,所以这实际上是溢出。 - M.M
显示剩余3条评论
6个回答

14
您可能需要阅读有关隐式转换的内容,特别是关于数字提升的部分,其中提到:

小整数类型(如char)的prvalue可以转换为大整数类型(如int)的prvalue。 特别是,算术运算符不接受比int更小的类型作为参数

上述内容表明,如果您在涉及算术运算符(当然包括乘法)的表达式中使用比int(如unsigned short)还要小的东西,则这些值将会被提升为int


6
这不是一个设计缺陷吗?特别是因为无符号类型的溢出被定义为行为,而有符号类型的溢出不是?我可以理解 charshort 会被提升为有符号的 int,但我本来期望 unsigned charunsigned short 会被提升为 unsigned int 以允许定义的溢出...或者我理解错了,将无符号的 short 算术运算结果分配给有符号的 int 并安全地转换为 unsigned short 是不会引起 UB 的吗? - Simon Kraemer
@SimonKraemer 或许是这样,但现在已经为时过晚了。这种情况始于C语言的最初几天,当时任何小于“int”的整数都存储在一个与int相同大小的寄存器中。(很可能这种行为来自C语言的前身之一) - M.M
@M.M 我想你是对的。我刚刚开了另一个问题,专门分析这个行为:https://dev59.com/mVsX5IYBdhLWcg3wf_r3 - Simon Kraemer

11
这是“常规算术转换”在起作用。
通常称为参数“提升”,尽管标准更加严格地使用该术语(合理的描述性术语和标准之间的永恒冲突)。
C++11 §5/9:
很多二元操作符期望算术或枚举类型的操作数,会引起转换并以类似方式产生结果类型。目的是产生一个公共类型,这也是结果的类型。此模式称为“常规算术转换”[…]。
段落继续描述细节,这些细节包括将不同类型转换为更一般的类型,直到所有参数都可以表示。此梯子上最低的一级是二进制操作的两个操作数进行整数提升,因此至少执行了这一步(但可以从较高的级别开始转换)。而整数提升始于这个:
C++11 §4.5/1:
排除布尔值、char16_t、char32_t或wchar_t之外的整数类型的prvalue,其整数转换等级(4.13)小于int的等级,如果int可以表示源类型的所有值,则可以将其转换为int的prvalue;否则,源prvalue可以转换为unsigned int的prvalue 至关重要的是,这是关于类型而不是算术表达式的。在您的情况下,乘法运算符* 的参数转换为int。然后作为int乘法执行乘法,产生一个int结果。

我认为OP在这里是安全的,因为如果int无法完全表示short int,则会将其转换为unsigned int(或者至少可以转换,无论你如何解释),正如您第二个标准引用中所写的那样。 - this
@this: 嗯,int乘法可能会溢出,这在形式上是未定义行为。编译器可能会“利用”这一点。实际上,编译器的程序员可以推断出,可以始终假定UB不会发生(因为如果它确实发生了,则任何效果都是有效行为),然后在这种假设下进行优化,会导致某些晦涩的情况下节省一两个纳秒的令人困惑的行为。 - Cheers and hth. - Alf

6
正如Paolo M在评论中指出的那样,USHRT_MAX的类型为int(这由5.2.4.2.1 / 1指定:所有这种宏的类型都至少与int一样大)。
因此,USHRT_MAX * USHRT_MAX已经是int x int,不需要进行任何提升。
这将在您的系统上引发有符号整数溢出,导致未定义的行为。
关于建议的解决方案:
unsigned u = static_cast<unsigned>(t*t);

这并没有帮助,因为 t*t 本身由于有符号整数溢出而导致未定义行为。正如其他答案所解释的,由于历史原因,在乘法发生之前,t 被提升为 int

相反,您可以使用以下方法:

auto u = static_cast<unsigned int>(t) * t;

在经过整数提升后,这是一个unsigned int与一个int相乘的结果;然后根据其余的通常的算术转换规则int会被提升为unsigned int,从而进行定义明确的模乘运算。


其他运算符呢?这样可以吗?ulong i = x + static_cast<ulong>(y)*m_mapSize.getX(),其中xygetX()都是unsigned short inti的类型是如何推断出来的? - Slazer
@Slazer,i 的类型是 ulong,因为你这么说了。任何运算符的结果类型都取决于它的两个操作数。在你的代码中,* 的操作数是 ulongushort;根据提升规则,后者被提升为 int,然后再提升为 ulong,得到一个 ulong 结果。然后 + 的操作数是 ushortulong,所以再次将 ushort 提升为 ulong,得到一个 ulong 结果。 - M.M

5

根据整数提升规则,USHRT_MAX的值会被提升为int类型。随后,我们对这两个int类型数进行乘法运算(可能会发生溢出)。


4

似乎还没有人回答这个问题的一部分:

我应该预期这种解决方案会有什么问题吗?

u = static_cast<unsigned>(t*t);

是的,这里存在问题:它首先计算t*t并允许它溢出,然后将结果转换为unsigned。根据C++标准,整数溢出会导致未定义的行为(即使在实践中它可能始终正常工作)。正确的解决方案是:

u = static_cast<unsigned>(t)*t;

请注意,由于第一个操作数是无符号的,第二个 t 在乘法之前被提升为 unsigned

你还应该注意,虽然在大多数当前平台上int比short大,但C标准并不保证它比short更大。 - plugwash
标准的那部分可能需要更清晰,但我非常确定它的意思是“相同大小或更大”,而不是“严格更大”。如果不是这样,那么我见过的每个C编译器都将不符合标准。 - plugwash
@plugwash 引用:“具有较小整数转换等级的类型的值范围是另一种类型值范围的子范围。” 如果这还不够清楚,下一段文字将详细解释子范围的含义。 - this
如果你的理解是正确的,那么long long需要比long更大,long需要比int更大,而int需要比short更大。你能说出一个平台符合这种情况吗? - plugwash
@plugwash 您错误地将等级和范围等同起来。等级被定义为严格更大或更小,但范围不是。子范围可以等于范围。等级或long long始终大于long(6.3.1.1,第1段),但这些类型的范围可以相同。(您可能希望使用@符号,否则我将不知道您已回复。) - this
显示剩余4条评论

3

正如其他答案所指出的那样,这是由于整数提升规则导致的。

避免将比具有较大等级的有符号类型小的无符号类型转换时,最简单的方法是确保将转换转换为unsigned int而不是int

这可以通过乘以类型为无符号整型的值1来完成。由于1是乘法恒等式,结果将保持不变:

unsigned short c = t * 1U * t;

首先,操作数 t 和 1U 被计算出来。左操作数是有符号的,并且比无符号的右操作数的等级小,因此它会被转换为右操作数的类型。然后进行乘法运算,结果与剩余的右操作数发生相同的情况。以下是标准中引用的最后一段内容。
对于两个操作数,整数提升首先被执行。然后将规则应用于提升后的操作数:
- 如果两个操作数具有相同的类型,则不需要进一步的转换。 - 否则,如果两个操作数都具有有符号整数类型或都具有无符号整数类型,则具有较低整数转换等级的操作数将被转换为具有更高等级的操作数的类型。 - 否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数的类型的等级,则具有有符号整数类型的操作数将被转换为具有无符号整数类型的操作数的类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接