【问题标题】:Why does C++ standard specify signed integer be cast to unsigned in binary operations with mixed signedness?为什么 C++ 标准指定有符号整数在具有混合符号的二进制操作中强制转换为无符号?
【发布时间】:2017-09-06 06:25:20
【问题描述】:

C 和 C++ 标准规定,在相同级别的有符号整数和无符号整数之间的二进制运算中,有符号整数被强制转换为无符号整数。由此引起的关于 SO 的问题很多......我们称之为奇怪的行为: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/

查看我自己的代码,无论我在哪里组合有符号和无符号整数,我总是需要从无符号转换为有符号。有些地方无关紧要,但我还没有找到一个可以将有符号整数转换为无符号的代码示例。

在哪些情况下可以正确执行无符号转换?为什么标准是这样的?

【问题讨论】:

  • 假装它是 1970 年代 - 想想迪斯科。有符号有 3 个品种,2 的补码,1 的补码和有符号的量级。无符号只有 1 种。关于混合类型的规则已经够糟糕了。然而,通过转为有符号类型,结果变得更加复杂,因为规则需要 3 个变体目的地,而不是转为无符号时的 1 个。
  • @chux,我认为推理可能与此有关。但是为什么你需要三个规则呢?如果将 -1 转换为无符号可以在 3 种有符号整数中以单一、明确的方式完成,为什么不能以单一、明确的方式将 0xFFFF 转换为无符号? (请注意,我正在考虑 16 位整数,因为它是 70 年代和所有。)
  • C 语言 - 正如其他人所说的那样,发明了这条规则 - 只有少数数据结构 - 裸数组、结构和指针 - 与当时的简单机器寻址模式相匹配- 诸如索引、间接、基础+位移、基础+位移+索引之类的东西。现在.. 确实,当时标准机器中的索引寻址模式将采用有符号整数......但当时的程序员并不经常使用负数来索引......而且他们 确实在他们的 16 位机器上需要 2 倍的无符号范围(尤其是在单词字段中)
  • Chris:该标准仅保证无符号值具有与具有相同等级的有符号值一样多的值位,而不是将符号位重新解释为值位。因此,您可以通过将无符号值的符号位强制为正来处理非 2s 补码架构,从而将可表示值的范围限制为有符号类型中可表示的正值范围。这使得两个转换都变得简单(无符号到有符号变为无操作或掩码)。但这也意味着将始终选择带符号的类型进行标准转换。
  • “我总是需要从无符号转换为有符号。” - 请记住,当无符号的值大于有符号类型的最大值时,这是不可移植的。编译器发出警告是有原因的;通过在这里投射,您是在说“哦,这永远不会发生”

标签: c++ c casting


【解决方案1】:

如果值无法表示,则从无符号转换为有符号会导致实现定义的行为。从有符号转换为无符号始终是无符号位大小的幂的模 2,因此它始终是明确定义的。

如果每个可能的无符号值都可以在有符号类型中表示,则标准转换为有符号类型。否则,选择无符号类型。这保证了转换始终是明确定义的。


注意事项

  1. 如 cmets 所述,C++ 的转换算法继承自 C 以保持兼容性,这在技术上是 C++ 中的原因。

  2. 撰写本说明时,C++ 标准允许三种二进制表示,包括符号大小和反码。情况不再如此,而且有充分的理由相信在合理的未来 C 也不会如此。我将脚注作为历史遗迹留下,但它与当前语言没有任何关系。

    有人建议,标准中定义有符号到无符号转换而不是无符号到有符号转换的决定在某种程度上是任意的,并且其他可能的决定是对称的。但是,可能的转换是对称的。

    在标准考虑的两种非 2 的补码表示中,n 位有符号表示只能表示 2n -1 个值,而 n 位无符号表示可以表示 2n 个值。因此,有符号到无符号的转换是无损的并且可以反转(尽管永远不能产生一个无符号值)。另一方面,无符号到有符号的转换必须将两个不同的无符号值折叠到同一个有符号结果中。

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

【讨论】:

  • “如果值无法表示,则从无符号转换为有符号会导致未定义的行为。” 不。
  • 但是一个演员表是定义的,而另一个不是因为它在标准中是这样写的。如果他们愿意,他们可以将演员定义为带符号的 int,或者? (顺便说一句:downvote 不是我的)
  • 保留 -1,因为这肯定不是 C++ 标准使用该定义的原因。
  • @chris:限制来自于假设某些架构会捕获溢出,并且该标准传统上避免规范,这些规范会强制执行额外检查以避免陷阱。
  • "如果每个可能的无符号值都可以在有符号类型中表示,则标准转换为有符号类型。否则,选择无符号类型。这保证了转换始终是明确定义的。” 而且,这种推理是无稽之谈。在编写标准时,您也可以很好地定义相反的结果。
【解决方案2】:

这是一个半答案,因为我不太了解委员会的推理。

来自 C90 委员会的理由文件:https://www.lysator.liu.se/c/rat/c2.html#3-2-1-1

自 K&R 发布以来,C 的实现在积分提升规则的演变过程中出现了严重的分歧。实现分为两大阵营,其特点可能是无符号保存值保存。这些方法之间的区别集中在 unsigned charunsigned short 的处理上,当被 整体提升 扩大时,该决定也会对常量的类型产生影响(参见 §3.1. 3.2)。

... 显然还有为匹配任何运算符的两个操作数所做的转换。它继续:

这两种方案在绝大多数情况下都给出了相同的答案,并且在使用二进制补码算术和无符号溢出的安静环绕的实现中甚至在更多情况下都给出了相同的有效结果——也就是说,在大多数当前的实现中.

然后它指定了一个解释出现歧义的情况,并指出:

结果必须被称为可疑签名,因为可以对已签名或未签名的解释进行案例。当unsigned int 在运算符中遇到signed int 并且signed int 具有负值时,就会出现完全相同的歧义。 (在解决这种冲突的模糊性方面,这两种方案都没有更好或更糟。)突然,否定的signed int 变成了一个非常大的unsigned int,这可能令人惊讶 --- 或者它可能正是我们想要的由知识渊博的程序员。当然,所有这些歧义都可以通过明智地使用转换来避免。

和:

无符号保留规则大大增加了unsigned int 面对signed int 以产生有问题的签名结果的情况的数量,而价值保留规则则最大限度地减少了这种冲突。因此,价值保留规则被认为对新手或粗心的程序员更安全。经过多次讨论,委员会决定支持值保留规则,尽管 UNIX C 编译器已经朝着无符号保留的方向发展。

因此,他们认为int + unsigned 的情况是一种不受欢迎的情况,并为charshort 选择了尽可能少产生这些情况的转换规则,即使当时大多数编译器都遵循一种不同的方法。如果我理解正确,那么这个选择会迫使他们遵循当前选择的int + unsigned 产生unsigned 操作。

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

【讨论】:

  • Cris:这个推理是针对不同的问题。我打算将它包含在我的答案中,但您特别询问了有符号和无符号 int 之间的转换,而您引用的辩论是关于推广比 int 更窄的无符号类型。这在 K&R C 中没有出现,因为 K&R C 除了unsigned int (和位域,但那是另一锅鱼)之外没有无符号整数。委员会实际上并不太关心signed + unsigned;有问题的情况是signed < unsigned(也是除法,但在实践中很少见)。
  • 因为大多数实现都是/曾经是隐式模运算的 2s 补码。因此,在常见情况下,除类似除法的运算符外,算术运算符的观察行为没有差异。对于比较运算符,将有符号转换为无符号或将无符号转换为(模块化)有符号非常糟糕。 (解决方案是(在 K&R 中)将两者都转换为(签名)long,但这需要由程序员决定。)
  • 无论如何,正如我之前所说,signed->unsigned 是明确定义的,unsigned->signed 是依赖于实现的决定已经做出。您可以争辩说可能会做出不同的决定,但是有很多因素会影响这个特定的决定。您也可以争辩说,C 永远不应该尝试容纳非 2s 补码机器。回想起来,这可能是合理的,但在当时,未来似乎并不是那么一维的。 从一开始 unsigned 就是纯粹的二进制 2s 补码。
  • 值得注意的是,当时最杰出的非 2 补码机器——IBM 7090/7094——使用符号幅度表示进行算术运算,但也有无符号进位传播加法(和也使用无符号模加法器进行地址计算)。因此,它可以容纳具有单个无符号宽度和符号幅度有符号类型的 C 实现。但是,afaik,没有为它编写任何 C 实现。
  • 无符号保留通常更好的原因是编写正确的混合操作更容易/更干净。例如,正确比较所有情况下的有符号和无符号值变为s < 0 || s < us >= 0 && s > u。这两个都依赖于这样一个事实,即如果无符号到有符号的转换会溢出,则第二个比较会将有符号转换为无符号。
【解决方案3】:

如果选择了 signed cast,那么简单的 a+1 总是会产生有符号类型(unless 常量被键入为 1U)。 p>

假设aunsigned int,那么在arr[a+1] 的情况下,这个看似无辜的增量a+1 可能会导致未定义溢出或“索引越界”等情况

因此,“无符号强制转换”似乎是一种更安全的方法,因为人们可能根本没想到会发生强制转换,而只是添加一个常量。

【讨论】:

  • 有趣的想法。当然,a 如果是 8 位整数,首先会被转换为 int,但如果 aunsigned int,那么它肯定是有意义的。
  • 我要把它改成无符号整数。我忘记了整数提升。
【解决方案4】:

为什么 C++ 标准在具有混合符号的二进制运算中指定有符号整数转换为无符号?

我想你的意思是 converted 而不是“cast”。强制转换是一种显式转换。

由于我不是作者,也没有遇到有关此决定的文档,因此我不能保证我的解释是真实的。但是,有一个相当合理的潜在解释:因为这就是 C 的工作方式,而 C++ 是基于 C 的。除非有机会改进规则,否则没有理由改变什么有效,什么程序员已经习惯了。我不知道委员会是否考虑过改变这一点。


我知道您可能在想什么:“为什么 C 标准指定有符号整数...”。好吧,我也不是 C 标准的作者,但至少有一个相当广泛的文档,标题为 “Rationale for 美国国家标准 信息系统 - 编程语言 - C"。尽管它很广泛,但遗憾的是它没有涵盖这个问题(它确实涵盖了一个非常相似的问题,即如何促进比int 更窄的整数类型,在这方面标准与某些 C早于标准的实现)。

我无法访问准标准 K&R 文档,但我确实找到了《专家 C 编程:深度 C 秘密》一书中的一段话,其中引用了准标准 K&R C 中的规则(在比较标准规则):

第 6.6 节算术转换

许多运算符以类似的方式导致转换和产生结果类型。这种模式将被称为“通常的算术转换”。

首先,任何 char 或 short 类型的操作数都转换为 int,任何类型的 float 都转换为 double。然后,如果任一操作数为双精度,则另一个将转换为双精度,这就是结果的类型。否则,如果任一操作数为 long,则将另一个转换为 long,这就是结果的类型。否则,如果其中一个操作数是无符号的,则另一个将转换为无符号,这就是结果的类型。否则,两个操作数都必须是 int,这就是结果的类型。

因此,这似乎是自 C 标准化之前以来的规则,并且可能是设计者自己选择的。除非有人能找到书面理由,否则我们可能永远不会知道答案。


在哪些情况下可以正确执行无符号转换?

这是一个极其简单的案例:

unsigned u = INT_MAX;
u + 42;

文字 42 的类型是有符号的,因此根据您提出的/设计者规则,u + 42 也将是有符号的。这将是非常令人惊讶的,并且会导致所示程序由于有符号整数溢出而具有未定义的行为。

基本上,隐式转换为有符号和无符号都有各自的问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-02-17
    • 1970-01-01
    • 2017-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-25
    • 2012-01-09
    相关资源
    最近更新 更多