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

use*_*044 5 c linux gcc posix clang

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

带符号的整数类型,宽度为N,无填充位和二进制补码表示.

它不会留下这个实现来定义,并且绝对不允许将它视为无界整数.

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$
Run Code Online (Sandbox Code Playgroud)

显然,这里有一个问题......它是什么?
我错过了某种隐性演员吗?

tux*_*ux3 9

我仍然会喊出不明确的行为.

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

所以当它看到你的功能时:

int stupid (int64_t a) {
  return (a+1) > a;
}
Run Code Online (Sandbox Code Playgroud)

它假定你永远不会打电话给它a==INT64_MAX,因为那将是UB.

因此,这个功能可以简单地优化为:

int stupid (int64_t a) {
  return 1;
}
Run Code Online (Sandbox Code Playgroud)

然后可以适当地内联.

我建议你阅读每个C程序员应该知道的关于未定义行为的内容,以获得有关编译器如何利用UB进行优化的更多解释.

  • *格式*对于特定类型是2s补码并不意味着*overflow*对于特定操作是2s补码.编译器可以自由地假设`INT64_MAX + 1`产生一个魔术结果,允许其优化有效. (5认同)

pax*_*blo 6

你不需要去POSIX对它进行排序,ISO C控制这个特定的方面(下面的参考是C11标准).

这个答案的其余部分将成为所有"语言律师",以显示为什么将未添加的行为添加到已签名的值中,以及为什么两个答案(真假)都有效.


首先,您int64_t在ISO中未定义的争用并不十分正确.科7.20.1.1 Exact-width integer types州,当提到intN_t类型,即:

typedef名称指定有符号整数类型,其宽度为N,无填充位和二进制补码表示.因此,表示这样的带符号整数类型,其宽度恰好为8位.intN_tint8_t

这些类型是可选的.但是,如果实现提供宽度为8,16,32或64位的整数类型,没有填充位,并且(对于具有二进制补码表示的有符号类型),它定义相应的typedef名称.

这就是为什么你不需要担心POSIX以某种方式定义这些类型的原因,因为ISO定义它们完全相同(两个补码,没有填充等),假设它具有适当的能力.


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

如果在计算表达式期间发生异常情况(即,如果结果未在数学上定义或未在其类型的可表示值范围内),则行为未定义.

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

如果两个操作数具有相同的类型,则不需要进一步转换.

在同一部分的早期,您会找到一个声明,该声明在找到常见类型后指示结果的类型:

除非另有明确说明,否则共同实数类型也是结果的对应实数类型.

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


根据您的注释,编码int64_t指示添加一个包装,您必须理解,它不会更改它根本未定义的子句.在这种情况下,实现仍然可以自由地执行任何操作,即使根据您的想法没有意义.

并且,在任何情况下,表达式INT64_MAX + 1 > INT64_MAX(其中1经历整数提升为a int64_t)可以简单地编译为,1因为可以说比实际递增值和进行比较更快.这正确的结果,因为任何东西都是正确的结果:-)

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

int ub (int i) { return i++ * i++; } // big no-no
:
int x = ub (3);
Run Code Online (Sandbox Code Playgroud)

进入更简单,几乎肯定更快:

int x = 0;
Run Code Online (Sandbox Code Playgroud)

您可能认为答案会更好9或者12(取决于++执行副作用的时间)但是,如果未定义的行为是打破编码器和编译器之间的契约,编译器可以自由地做任何想做的事情.


在任何情况下,如果你想要一个定义良好的函数版本,你可以选择以下内容:

int stupid (int64_t a) {
    return (a == INT64_MAX) ? 0 : 1;
}
Run Code Online (Sandbox Code Playgroud)

这可以在诉诸未定义的行为的情况下获得所需/预期的结果:-)


1可能在这里是边缘情况下,如果的宽度int实际上是大于64位.在这种情况下,整数促销很可能会强制转换int64_t为a int,从而可以很好地定义表达式.我没有详细研究过,所以可能是错的(换句话说,不要把它视为我答案中的福音部分)但是值得记住的是要检查一下是否有int更多的实现超过64位宽.

  • @ user3710044:不,两个补码的重点是除了符号位本身之外,签名值不需要特殊处理.与符号幅度表示的加法相比较,当符号位一致时它们与它们不同时需要不同的逻辑. (3认同)
  • @ user3710044:两个补码的添加根本没有"明确定义".有很多硬件实现*饱和*二进制补码. (2认同)
  • @ user3710044:无符号饱和加法和二进制补码加法的硬件也一样好.唯一的区别是饱和是由进位标志还是溢出标志触发的.您甚至可以争辩说,进位标志与溢出标志是无符号模块加法和二进制补码模块加法之间的微小差别......但实际上电路只是生成两者. (2认同)