为什么C++标准规定在混合符号二进制操作中,有符号整数要转换为无符号整数?

11
C和C++标准规定,在有符号和无符号整数的二进制操作中,如果这两个整数等级相同,那么有符号整数会被强制转换为无符号整数。对此有许多stackoverflow上的问题,我们称之为奇怪的行为:unsigned to signed conversionC++ Implicit Conversion (Signed + Unsigned)A warning - comparison between signed and unsigned integer expressions% (mod) with mixed signedness等。
但是,这些问题都没有给出任何理由,说明为什么标准要这样做,而不是将其转换为有符号整数。我找到了一位自称专家,他说这是显然正确的事情,但他也没有给出解释:http://embeddedgurus.com/stack-overflow/2009/08/a-tutorial-on-signed-and-unsigned-integers/
在我的代码中查找时,无论我组合有符号和无符号整数的地方,我总是需要从无符号转换为有符号。虽然也有一些地方不重要,但我没找到一个唯一的代码示例,其中将有符号整数转换为无符号整数是有意义的。
什么情况下将其转换为无符号是正确的?为什么标准是这样的?

7
假装是20世纪70年代,想象迪斯科。有三种有符号的类型:二进制补码、一的补码和原码。无符号类型只有一种。混合类型的规则已经很复杂了。但是如果使用有符号类型,结果就会变得更加复杂,因为需要用到三个不同的目标类型,而不是使用无符号类型时只需要一个目标类型。 - chux - Reinstate Monica
1
@chux,我认为这种推理可能与此有关。但是为什么需要三个规则呢?如果将-1转换为无符号可以在3种有符号整数的单一和明确定义的方式下完成,为什么不能将0xFFFF转换为无符号并以单一和明确定义的方式完成呢?(请注意,我考虑的是16位整数,因为那是70年代。) - Cris Luengo
2
C语言 - 正如其他人所说,这个规则是在这里发明的 - 只有几种数据结构 - 裸数组、结构体和指针 - 这与当时简单的机器寻址模式相匹配 - 例如索引、间接、基址+位移、基址+位移+索引。现在...确实,在当时标准机器中的索引寻址模式会使用有符号整数...但同样也是事实,当时的程序员并不经常使用负数进行索引...而且他们确实需要16位机器上无符号整数的2倍范围(尤其是在字字段中)。 - davidbak
1
Chris:标准仅保证无符号值具有与相同等级的有符号值一样多的值位,而不是将符号位重新解释为值位。因此,您可以通过强制无符号值的符号位为正来处理非2s补码架构,从而限制可表示值的范围为有符号类型中可表示正值的范围。这使得转换变得简单(无符号到有符号的转换成为无操作或掩码)。但这也意味着有符号类型将始终被选择用于标准转换。 - rici
2
我总是需要从无符号转换为有符号。请注意,当无符号值大于有符号类型的最大值时,这是不可移植的。编译器有警告是有原因的;通过在此处进行强制转换,您正在表明“哦,那永远不会发生”。 - M.M
显示剩余11条评论
4个回答

12
如果无法表示该值,则从无符号类型转换为有符号类型会导致实现定义的行为。从有符号类型转换为无符号类型始终是模2的无符号位数的幂,因此始终是明确定义的。 如果有符号类型可表示每个可能的无符号值,则标准转换为有符号类型。否则选择无符号类型。这保证了转换始终是明确定义的。
注: 1. 如评论所示,C++ 的转换算法继承自 C 以保持兼容性,这在技术上是其如此的原因。 2. 当写下这个注释时,C++ 标准允许三种二进制表示,包括符号-大小和补码。现在不再是这种情况,我们有理由相信在可预见的未来 C 也不会是这种情况。我将注脚留作历史遗迹,但它与当前语言无关。 3. 已经有人建议,在标准中定义有符号到无符号转换而不是无符号到有符号转换是某种方式任意的决定,并且其他可能的决定将是对称的。然而,这些可能的转换不是对称的。 在标准中考虑的两种非二的补码表示中,n 位有符号表示仅能表示 2^n−1 个值,而 n 位无符号表示可以表示 2^n 个值。因此,由有符号到无符号的转换是无损的,并且可以反转(尽管一个无符号值永远不能被产生)。另一方面,无符号到有符号的转换必须将两个不同的无符号值合并成相同的有符号结果。

在评论中,提出了公式 sint = uint > sint_max ? uint - uint_max : uint。这个公式将值 uint_max 和 0 合并起来,都映射为0。即使对于非二进制补码表示法,这也有点奇怪,但对于二进制补码表示法来说,这是不必要的,更糟糕的是,它需要编译器发出代码来费力地计算这个不必要的混合。相比之下,标准的有符号到无符号转换是无损的,在常见情况下(二进制补码架构)它是一个空操作。


2
将无符号类型转换为有符号类型,如果该值无法表示,则会导致未定义的行为。不行。 - Baum mit Augen
但是一个强制转换已经被定义,而另一个没有被定义,因为它在标准中就是这样写的。如果他们想要的话,他们本可以将强制转换定义为有符号整数,或者?(顺便说一下:我不是给它点踩的) - Cris Luengo
1
@chris: 这个限制来自于一些架构会捕获溢出的假设,而且标准传统上避免了会强制加入额外检查以避免陷阱的规范。 - rici
1
标准转换是将每个可能的无符号值表示为有符号类型,如果可以,则选择有符号类型。否则,选择无符号类型。这保证了转换始终是明确定义的。此外,这种推理是无意义的。在编写标准时,您同样可以使相反的结果明确定义。 - Baum mit Augen
@chris 我不认为溢出陷阱很常见,因此无法进行有意义的比较。但是,如果机器支持无符号算术,在尝试将有符号值用作无符号值时,它不会触发陷阱。另一方面,如果存在负零并且无符号值使用符号位,则无符号值可能会变成一个陷阱负零。 - rici
显示剩余5条评论

3
如果选择了“有符号强制转换”,那么简单的a+1将始终导致有符号类型(除非常量被输入为1U)。
假设aunsigned int,那么这个看似无害的增量a+1可能会导致未定义的溢出或“索引越界”,在arr[a+1]的情况下。
因此,“无符号强制转换”似乎是一种更安全的方法,因为人们可能根本不希望在简单地添加一个常量时发生强制转换。

有趣的想法。当然,如果a是8位整数,那么首先会将其转换为int,但如果aunsigned int,那么这肯定是有意义的。 - Cris Luengo
我打算将它改为无符号整型。我短暂忘记了整数提升。 - Radzor

1
为什么C++标准规定混合有符号和无符号的二进制运算中,有符号整数应转换为无符号整数?
我认为你的意思是“转换”而不是“强制转换”。强制转换是显式转换。
由于我既不是作者也没有遇到这个决定的文档,所以我不能保证我的解释是真实的。然而,有一个相当合理的潜在解释:因为这就是C语言的工作方式,而C++是基于C的。除非有机会改进规则,否则没有理由改变已经行之有效并且程序员已经习惯的东西。我不知道委员会是否甚至考虑过更改这一点。
我知道你可能会想:“为什么C标准规定带符号整数...”。好吧,我也不是C标准的作者,但至少有一份相当详尽的文件,名为“美国国家标准信息系统-编程语言-C的原理”,尽管它很详尽,但不幸的是它没有涉及到这个问题(它确实涉及了一个非常类似的问题,即如何提升窄于int的整数类型,在这方面,标准与一些在标准之前的C实现不同)。
我没有访问预先制定的K&R文档,但我找到了一段摘自书籍《专家C编程:深入C的秘密》的文字,引用了来自预先制定的K&R C的规则(在比较该规则与标准化规则的上下文中)。

第6.6节 算术转换

许多操作符会引起类型转换并以类似的方式产生结果类型。这种模式将被称为“通常的算术转换”。

首先,任何 char 或 short 类型的操作数都会被转换为 int 类型,任何 float 类型的操作数都会被转换为 double 类型。然后,如果任一操作数为 double,则另一个操作数将被转换为 double,并且那就是结果的类型。否则,如果任一操作数为 long,则另一个操作数将被转换为 long,并且那就是结果的类型。否则,如果任一操作数为 unsigned,则另一个操作数将被转换为 unsigned 并且那就是结果的类型。否则,两个操作数必须都是 int,那就是结果的类型。

因此,看起来这已经是 C 标准化之前的规则了,很可能是由设计者本人选择的。除非有人能找到书面理由,否则我们可能永远不会知道答案。


何时将类型转换为无符号整数是正确的选择?

这里有一个非常简单的情况:

unsigned u = INT_MAX;
u + 42;

42这个字面量的类型是有符号的,因此按照您提出的/设计师规则,u + 42也将是有符号的。这将是非常令人惊讶的,并且由于有符号整数溢出而导致所示程序具有未定义的行为。
基本上,隐式转换为有符号和无符号都有各自的问题。

1
这是一个半回答,因为我并不真正理解委员会的推理。
从C90委员会的理由文件中得知:https://www.lysator.liu.se/c/rat/c2.html#3-2-1-1 自K&R出版以来,C实现在整数提升规则的演变中发生了严重分歧。 实现可以分为两个主要阵营,可以称为 unsigned preserving 和 value preserving 。 这些方法之间的差异集中在当通过整数提升扩展时,对 unsigned char 和 unsigned short 的处理上,但决定也对常量的类型有影响(见§3.1.3.2)。
...显然还有匹配任何运算符的两个操作数所做的转换。 它继续说:
两种方案在绝大多数情况下都给出相同的答案,在使用二进制补码算术和有符号溢出的安静环绕的实现中,甚至更多的情况下都会给出相同的有效结果——也就是说,在大多数当前的实现中。然后它指定了一种解释模糊的情况,并声明道:“结果必须被称为有问题的有符号,因为可以为有符号或无符号解释辩护。每当一个unsigned int在运算符上遇到一个signed int,而这个signed int具有负值时,就会产生完全相同的歧义。(无论哪种方案在解决这种对抗的歧义方面都没有做得更好或更差。)突然间,这个负的signed int变成了一个非常大的unsigned int,这可能令人惊讶——或者恰恰是一个知识渊博的程序员所期望的。当然,通过明智地使用强制类型转换,所有这些歧义都可以避免。

未签名保留规则大大增加了 unsigned intsigned int 相遇而产生可疑符号结果的情况,而值保留规则最小化了这种相遇。因此,尽管 UNIX C 编译器已经朝着未签名保留方向发展,但委员会还是决定采用值保留规则,认为这对于初学者或不谨慎的程序员更加安全。因此,他们认为 int + unsigned 的情况是不必要的,并选择了对 charshort 进行转换的规则,以尽可能少地出现这种情况,即使当时大多数编译器都采用了不同的方法。如果我理解正确,这个选择迫使他们遵循当前的 int + unsigned 产生无符号操作的选择。

我仍然觉得这一切真是奇怪。


2
Cris:那个推理是关于另一个问题的。我本来要在我的回答中包含它,但你特别询问了有符号和无符号int之间的转换,而你引用的辩论是关于比int窄的无符号类型的提升。这在K&R C中没有出现,因为K&R C除了unsigned int(和位域,但那是另一回事)之外没有无符号整数。委员会实际上并不太关心signed + unsigned;问题的棘手情况是signed < unsigned(还有除法,但在实践中很少出现)。 - rici
1
要理解这些引用,您需要欣赏委员会的工作方式。 C标准由一个委员会投票表决,该委员会的成员具有不一定兼容的各种既得利益。基本上,这些引语是在说,一些投票成员投票支持"值保留"转换,另一些则投票支持"符号保留",而双方都不愿意妥协。最终,投票支持值保留规则的派别获得了投票的优势。所以,我们得到的行为是出于政治而不是技术上的考虑。 - Peter
2
无符号保留通常更好的原因是,编写正确的混合操作更容易/更清晰。例如,对于所有情况正确地比较有符号和无符号值变为s < 0 || s < us >= 0 && s > u。这两个都依赖于第二个比较,如果无符号转有符号会溢出,则将有符号转换为无符号。 - Chris Dodd
1
@rici:在这些提案之间的一个折中方案是指定这样的转换可以由实现者根据自己的判断处理为有符号或无符号,并使用预定义的宏来指示特定实现将如何处理它们。如果允许这样做,最有可能将短无符号类型提升为无符号类型的实现将是那些不使用安静环绕的二进制补码语义的实现。因此,这种处理的理由有点站不住脚。同样具有讽刺意味的是,“现代”实现... - supercat
1
不再将(unsigned)((unsigned)ushort1)*ushort2视为等同于(unsigned)((int)ushort1*ushort2),即使当前大多数实现都将它们视为等同的事实在解释中被提到作为有符号短整型提升为有符号整型的动机因素。 - supercat
显示剩余7条评论

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