现在,你们中的一些人会想要大喊未定义的行为,但是有一个问题。类型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$
显然,这里存在一个问题......它是什么? 我是否错过了某种隐性演员?
答案 0 :(得分:10)
我仍然会大喊未定义的行为。
这里的推理很简单,编译器假定您是一个完美的程序员并且从不编写任何可能导致未定义行为的代码。
所以当它看到你的功能时:
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进行优化的更多解释。
答案 1 :(得分:6)
你不需要去POSIX对它进行排序,ISO C控制这个特定的方面(下面的参考文献是C11
标准)。
这个答案的其余部分将成为所有“语言律师”,以显示为什么在您的签名值中添加一个未定义的行为,以及为什么两个答案(true 和 false)有效。
首先,您在ISO中未定义int64_t
的论点并不正确。部分7.20.1.1 Exact-width integer types
在提及intN_t
类型时指出:
typedef名称
intN_t
指定有符号整数类型,宽度为 N ,无填充位和二进制补码表示。因此,int8_t
表示这样的带符号整数类型,其宽度恰好为8位。这些类型是可选的。但是,如果实现提供宽度为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
经历整数提升为int64_t
)可能只是编译为1
,因为这可以说更快要比实际递增一个值并进行比较。并且是正确的结果,因为任何是正确的结果: - )
从这个意义上讲,它与实现转换没有什么不同:
int ub (int i) { return i++ * i++; } // big no-no
:
int x = ub (3);
更简单,几乎肯定更快:
int x = 0;
你可以争辩答案会更好9
或12
(取决于执行++
副作用的时间)但是,如果未定义行为是打破编码器和编译器之间的契约,编译器可以自由地做任何想做的事情。
在任何情况下,如果您想要一个定义良好的版本的功能,您可以选择以下内容:
int stupid (int64_t a) {
return (a == INT64_MAX) ? 0 : 1;
}
在没有诉诸未定义行为的情况下,可以获得所需/期望的结果: - )
1 如果int
的宽度实际上大于64位,那么可能是边缘情况。在这种情况下,整数促销很可能会强制int64_t
到int
,从而可以很好地定义表达式。我没有详细研究过,所以可能是错的(换句话说,不要把它视为我答案的福音部分)但是值得记住的是要检查一下是否有一个实现{ {1}}超过64位宽。
答案 2 :(得分:4)
POSIX定义int64_t
的宽度和表示,但算术运算符(如+
)的行为由C标准定义。 C标准很清楚,如果结果溢出,算术运算符对有符号值的行为是不确定的。
答案 3 :(得分:4)
C116.5§5适用:
如果在评估表达式期间发生异常情况(即,如果结果未在数学上定义或未在其类型的可表示值范围内),则行为未定义。
不是问题类型的定义(即使没有POSIX,已经指定了精确宽度整数类型仅使用C标准的二进制补码),但结果是“签名加法”操作在特殊情况下。
如果您不想这样,请使用未签名的算术。请注意,如果要避免实现定义的行为,则需要通过类型化处理而不是强制转换来执行返回到signed的最终转换。
就我个人而言,我觉得这样做太过分了,并且可以很好地将比较写成
(int64_t)((uint64_t)a + 1) > a
而不是经历使用工会或指针演员的麻烦。
答案 4 :(得分:1)
2的补码表示的规范很重要,因为C在同一类型上提供逐位和算术运算,因此定义变量的逐位表示与其算术解释之间的关系很重要。
例如,将2的补码整数的最高有效位设置为1(逐位运算)相当于从其值中减去INT_MAX+1
(算术运算)。
这些操作不是相互定义的,而是根据数学逻辑定义的。没有“2的补充加法”这样的东西,因为“2的补码”是一个比特表示概念,而“加法”是算术概念 - 这些术语来自不同的域。
因此,定义这些关系不会自动定义单个操作的结果,这些操作在其自己的域中导致超出该类型范围的值。例如,需要比表示中定义的更多位的位移是未定义的,无论它与算术值的关系是否可能。在位表示域中,操作根本不可能。
出于同样的原因,无论在何时尝试按位运算,都会导致大于该类型允许的最大值的算术值的加法。在算术领域内,结果定义明确,但无法存储。
标准可以为逻辑上“不可能”的操作定义行为,例如除以零。在溢出的情况下,标准可以定义这个结果在“环绕”中,影响表示的最重要位。同样,它可能表示此操作应该导致某种运行时错误,或者将其定义为产生最大可能值。
C标准将其视为未定义的行为,这意味着编译器选择的任何行为都同样有效,即使它在不同的优化级别或程序的不同部分有所不同。在您的情况下,没有a
的值声明a+1 > a
不正确,因此编译器可以自由地假设所有可能值的不等式为真,并在编译时简化它。