这种对 int64_t 的处理是 GCC 和 Clang 错误吗?

Is this treatment of int64_t a GCC AND Clang bug?

现在,你们中的一些人可能会想大喊 未定义的行为,但是有一个问题。类型 int64_t 不是由 C 标准定义的,而是由 POSIX 定义的。 POSIX 将此类型定义为:

a signed integer type with width N, no padding bits, and a two's-complement representation.

它不会将其留给实现来定义,并且绝对不允许将其视为无界整数。

linux$ cat x.c
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

int stupid (int64_t a) {
  return (a+1) > a;
}

int main(void)
{
    int v;
    printf("%d\n", v = stupid(INT64_MAX));
    exit(v);
}

linux$ gcc -ox x.c -Wall && ./x
0
linux$ gcc -ox x.c -Wall -O2 && ./x # THIS IS THE ERROR.
1
linux$ gcc --version
gcc (Debian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

linux$ uname -a
Linux localhost 3.14.13-0-amd64 #1 SMP Sat Jul 26 20:03:23 BST 2014 x86_64 GNU/Linux
linux$ getconf LONG_BIT
32
linux$

很明显,这里有问题……是什么?
我是否错过了某种隐式转换?

我还是要喊未定义的行为

这里的推理很简单,编译器假设您是一个完美的程序员并且绝不会编写任何可能导致未定义行为的代码。

所以当它看到你的函数时:

int stupid (int64_t a) {
  return (a+1) > a;
}

它假定您永远不会用 a==INT64_MAX 调用它,因为那将是 UB。

因此,这个函数可以简单地优化为:

int stupid (int64_t a) {
  return 1;
}

然后可以根据需要内联。

我建议您阅读 What Every C Programmer Should Know About Undefined Behavior 以了解有关编译器如何利用 UB 进行优化的更多解释。

POSIX定义了int64_t的宽度和表示,但是+等算术运算符的行为是由C标准定义的。 C 标准很清楚,如果结果溢出,算术运算符对有符号值的行为是未定义的。

C11 6.5 §5 适用:

If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.

问题不在于类型的定义(即使没有 POSIX,精确宽度的整数类型已经被 C 标准单独指定为使用补码),而是操作'signed addition' 导致异常情况。

如果您不想这样,请改用无符号算术。请注意,如果您想避免实现定义的行为,则需要通过类型双关而不是强制转换来执行最终转换回签名。

我个人认为这有点矫枉过正,可以将比较写成

(int64_t)((uint64_t)a + 1) > a

而不是经历使用联合或指针转换的麻烦。

你不需要去 POSIX 来解决这个问题,ISO C 完全控制这个特定的方面(下面的参考是 C11 标准)。

这个答案的其余部分将交给所有“语言律师”来说明为什么将带符号的值加一是未定义的行为,因此 both 答案(真 false) 有效。


首先,您关于 int64_t 未在 ISO 中定义的说法并不正确。 7.20.1.1 Exact-width integer types 节指出,当提到 intN_t 类型时,即:

The typedef name int<i>N</i>_t designates a signed integer type with width N, no padding bits, and a two’s complement representation. Thus, int8_t denotes such a signed integer type with a width of exactly 8 bits.

These types are optional. However, if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two’s complement representation, it shall define the corresponding typedef names.

这就是为什么您无需担心 POSIX 以某种方式定义这些类型,因为 ISO 对它们的定义完全相同(二进制补码、无填充等),前提是它具有适当的功能.


所以,既然我们已经建立了 ISO 定义了它们(如果它们在实现中可用),现在让我们看看 6.5 Expressions /5:

If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.

添加两个相同的整数类型肯定会给你 相同的 类型(至少在 int64_t 的级别,远高于完成整数提升的点1),因为这是由 6.3.1.8 中指定的常用算术转换决定的。在处理各种浮点类型(其中 int64_t 不是)的部分之后,我们看到:

If both operands have the same type, then no further conversion is needed.

在同一部分的前面,您会发现在找到常见类型后,指示 结果 类型的语句:

Unless explicitly stated otherwise, the common real type is also the corresponding real type of the result.

因此,假设 INT64_MAX+1 的结果实际上不适合 int64_t 变量,则行为未定义。


根据您的评论,int64_t 的编码表示添加一个 换行,您必须了解 不会 更改它根本未定义的子句。在这种情况下,实现仍然可以自由地做任何它想做的事情,即使根据你的想法这没有意义。

而且,在任何情况下,表达式 INT64_MAX + 1 > INT64_MAX(其中 1 经过整数提升为 int64_t)可能会被简单地编译为 1,因为那是可以说比实际增加一个值并进行比较更快。它 正确的结果,因为 anything 是正确的结果:-)

从这个意义上说,它与实现转换没有什么不同:

int ub (int i) { return i++ * i++; } // big no-no
:
int x = ub (3);

更简单而且几乎肯定更快:

int x = 0;

您可以争辩答案会更好912(取决于执行++副作用的时间) 但是,鉴于未定义的行为破坏了编码器和编译器之间的契约,编译器可以自由地做任何它想做的事。


无论如何,如果您想要一个 定义良好的 版本的函数,您可以简单地选择如下内容:

int stupid (int64_t a) {
    return (a == INT64_MAX) ? 0 : 1;
}

这让你 desired/expected 结果 没有 求助于未定义的行为:-)


1 如果 int 的宽度实际上大于 64 位, 可能 是一个边缘情况。在那种情况下,整数提升很可能会将 int64_t 强制为 int,从而使表达式得到明确定义。我没有对此进行详细研究,所以可能是错误的(换句话说,不要将其视为我答案的福音部分)但值得牢记的是检查您是否曾经使用 int 超过 64 位宽。

2 的补码表示的规范很重要,因为 C 在同一类型上同时提供按位运算和算术运算,因此定义变量的按位表示与其算术解释之间的关系很重要。

例如,将 2 的补码整数的最高有效位设置为 1(按位运算)相当于从其值中减去 INT_MAX+1(算术运算)。

这些操作不是根据彼此定义的,但是,它们是根据数学逻辑定义的。没有“2的补码加法”之类的东西,因为“2的补码”是位表示概念,而"addition"是算术的-术语来自不同的领域。

因此,定义这些关系不会自动定义单个操作的结果,这些操作在它们自己的域内会产生超出类型范围的值。例如,需要比表示中定义的位数更多的位移位是未定义的,如果可能的话,无论它与算术值的关系如何。在比特表示的领域内,根本不可能进行运算。

出于同样的原因,导致算术值大于该类型允许的最大值的加法是未定义的,无论如果尝试它会产生什么按位运算。在算术领域内,结果是明确的,但无法存储。

标准可以为逻辑上 "impossible" 的操作定义行为,例如除以零。在溢出的情况下,标准 可以 定义这导致 "wraparound",影响表示的最高有效位。同样,它 可以 说这个操作应该导致某种运行时错误,或者将其定义为产生最大可能值。

C 标准将其保留为未定义的行为,这意味着编译器选择的任何行为都同样有效,即使它在不同的优化级别或程序的不同部分有所不同。在您的情况下,没有 a 的值可以断言 a+1 > a 不正确的 ,因此编译器可以自由地假设不等式对所有人都是正确的可能的值,并在编译时对其进行简化。