【问题标题】:Is 1 << 31 well defined in C when sizeof(int) == 4当 sizeof(int) == 4 时 1 << 31 在 C 中定义良好
【发布时间】:2017-12-29 07:46:30
【问题描述】:

根据this questions的回答:

E1 的结果是E1左移E2位位置;空出的位用零填充。如果 E1 具有无符号类型,则结果的值为 E1 × 2E2,比结果类型。如果 E1 有带符号类型和非负值,并且 E1 × 2E2 在结果类型中是可表示的,那么这就是结果值;否则,行为未定义。

这似乎暗示 1 &lt;&lt; 31 未定义。

但是,如果我使用1 &lt;&lt; 31,GCC 不会发出警告。 它确实为1 &lt;&lt; 32 发行了一份。 link

那是什么?我误解了标准吗? GCC有自己的解释吗?

【问题讨论】:

  • @BrianCain 不,应该作为 this 的副本关闭
  • 这里有两个单独的问题:(a) is 1 &lt;&lt; 31 undefined(许多现有问题已经涵盖),以及 (b) 为什么 gcc 不给出警告;这不是语言律师的问题。我建议将问题编辑为显然是这两个之一,但不是两者都
  • 监视 UB 是你的责任,而不是编译器的。编译器可能会也可能不会通过发出警告来帮助您。

标签: c language-lawyer undefined-behavior bit-shift


【解决方案1】:

否:如果 int 类型只有 31 个值位,1 &lt;&lt; 31 的行为未定义。

1U &lt;&lt; 31 正常,如果类型 unsigned int 具有 32 个值位,则计算结果为 0x80000000

在字节有 8 位的系统上,sizeof(int) == 4 意味着 int 最多有 31 个值位,因此将 1 移位 31 位是未定义的。相反,在CHAR_BIT &gt; 8 的系统上,写1 &lt;&lt; 31 可能没问题。

如果您提高警告级别,gcc 可能会发出警告。试试gcc -Wall -Wextra -W -Werrorclang 确实会使用相同的选项发出警告。

为了解决 Michaël Roy 的 cmets,1 &lt;&lt; 31 确实可靠地评估为 INT_MIN。它可能会在您的系统上给出这个值,但标准并不保证它,实际上标准将其描述为未定义的行为,因此您不仅不能依赖它,还应该避免它以避免虚假错误。优化器通常会利用潜在的未定义行为来删除代码并打破程序员的假设。

例如,以下代码可能编译为简单的return 1;

int check_shift(int i) {
   if ((1 << i) > 0)
       return 1;
   else
       return 0;
}

Godbolt's compiler explorer 支持的编译器都不支持,但这样做不会破坏一致性。

【讨论】:

  • @MichaëlRoy - 根据定义,它是未定义的。
  • @MichaëlRoy:它可能会在你的系统上给出这个值,但标准并不保证它,实际上标准将其描述为未定义的行为,所以你不仅不能依赖它,你应该避免它以防止虚假错误。优化器通常会利用潜在的未定义行为来删除代码并打破程序员的假设。
  • @MichaëlRoy:再说一次,我们这里不是在讨论 asm/硬件。我们正在谈论C语言。 C 编译器/优化器在后一个域中工作。如果你想要一个明确的方式来获得0x80000000 in C 那么你应该做1U &lt;&lt; 31
  • @MichaëlRoy 你混淆了位移和乘法。不,他不是。他实际上是引用 6.5.7 Bitwise shift operators, paragraph 4C standard。注意第 6.5.7 节的标题。
  • @MichaëlRoy:恐怕 C 标准的语言很清楚。之所以这样说,是因为并非所有计算机都是英特尔 PC。如果在不同的 CPU 上,将有符号值移位可能不会溢出到符号位或触发异常,CPU 可能不会使用二进制补码表示......1 &lt;&lt; 31 的行为会在这些 CPU 上给出不同的结果。这是标准将边界情况描述为未定义行为的典型原因。依赖 C 标准没有保证的行为的后果可能很难找到错误。
【解决方案2】:

GCC 没有对此发出警告的原因是因为1 &lt;&lt; 31 在 C90 中是有效(但由实现定义),并且 有效(但由实现定义) 即使在现代 C++ 中。 C90 将&lt;&lt; 定义为位移位,然后说对于无符号类型,它的结果是乘法,但对于有符号类型没有做这样的事情,这隐含地使它有效并让它被按位的一般措辞所涵盖运算符具有签名类型的实现定义方面。现在的 C++ 将&lt;&lt; 定义为乘以对应的无符号类型,结果转换回有符号类型,这也是实现定义的。

C99 和 C11 确实使此无效(即行为未定义),但允许编译器接受它作为扩展。为了与现有代码兼容,并在 C 和 C++ 前端之间共享代码,GCC 继续这样做,但有一个例外:您可以使用 -fsanitize=undefined 来检测到未定义的行为以在运行时中止您的程序,而这个确实处理 1 &lt;&lt; 31,但仅在编译为 C99 或 C11 时。

【讨论】:

  • 有趣。因此,这一定意味着 C++ 现在定义了从无符号到有符号的超出范围的转换,而这在以前是未定义的。我不知道:)
  • @OliverCharlesworth 据我所知,从另一种整数类型到有符号整数类型的超出范围转换,无论是有符号还是无符号,无论宽度相同还是更大,一直有效,但实现-在所有版本的 C 和 C++ 中定义。有些版本只允许实现定义的值,其他版本还允许引发实现定义的信号,但它永远不会未定义。
  • 我会说,在 C90 中,对于这种情况,它的指定不足或有缺陷。与 C++14 不同,例如 C++14 专门涵盖了实现定义的情况
  • @MM C90 似乎在&lt;&lt; 的描述中未指定它,但有一个通用条款指出二元运算符具有符号类型的实现定义方面(直接在“表达式”下) .这也适用于这里,这就是为什么它不是简单地被遗漏定义的原因。我将编辑我的答案以涵盖这一点。
  • @BenVoigt 它被视为 C++11 CWG #1457 中的一个缺陷,因此追溯应用了更改。至于 C++03,这主要使用 C90 措辞进行左移操作,但我没有找到匹配的措辞来说明按位运算符对有符号类型具有实现定义的方面。如果该措辞不存在,我认为您的观点很好。
【解决方案3】:

它确实会调用未定义的行为,正如其他答案/cmets 所解释的那样。但是,至于为什么 GCC 不发出诊断信息。

实际上有两件事会导致左移的未定义行为(均来自 [6.5.7]):

  1. 如果右操作数的值为负数或大于或等于提升的左操作数的宽度,则行为未定义。

  2. 如果 E1 具有带符号类型和非负值,并且 E1 × 2E2 在结果类型中是可表示的,那么这就是结果值;否则,行为未定义。

显然 GCC 会检测到第一个(因为这样做很简单),但不是后者。

【讨论】:

  • 另一种解释可能是 gcc 在这种情况下的行为是“可预测的”,并且 gcc 开发人员不想通过警告用户对 gcc 上未损坏的代码(针对所选目标)发出警告。警告可能在其他平台或编译器上中断的代码不是他们的责任
  • C++ 标准的作者出于某种原因决定将 1&lt;&lt;(bitsize-1) 定义为在二进制补码机器上产生 ~INT_MAX,即使它涉及溢出,但决定不定义负值的左移即使在这样的机器上,即使在不涉及溢出的情况下。
猜你喜欢
  • 2019-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-05
  • 1970-01-01
  • 2011-03-07
  • 2011-01-16
  • 2014-12-07
相关资源
最近更新 更多