【问题标题】:How can (x+1) > x evaluate to both 0 and 1?(x+1) > x 如何计算为 0 和 1?
【发布时间】:2021-05-28 18:05:54
【问题描述】:

我正在学习未定义的行为,并在没有任何明确解释的情况下偶然发现了这段代码:

#include <stdio.h>
#include <limits.h>

int foo ( int x) {
    printf ("% d\n" ,  x );   //2147483647
    printf ("% d\n" ,  x+1 ); //-2147483648  overflow
    return ( x+1 ) > x ;      // 1 but How????
}

int main ( void ) {
    printf ("% d\n" ,  INT_MAX );     //2147483647
    printf ("% d\n" ,  INT_MAX+1 );   //-2147483648  overflow
    printf ("% d\n" , ( INT_MAX+1 ) > INT_MAX );  //0  makes sense, since -ve < +ve
    printf ("% d\n" ,  foo(INT_MAX) );  //1
    return 0;
}

在 gcc 上编译时,编译器发出警告:

警告:“int”类型表达式中的整数溢出导致“-2147483648”

所以,INT_MAX+1 的值显然是负数,这就解释了为什么 (INT_MAX+1) &gt; INT_MAX 的计算结果为 0。

但是,对于foo(...) 中的x = INT_MAX,为什么(或如何)(x+1) &gt; x 评估为 1

【问题讨论】:

  • MAX_INT 是在哪里定义的,它的定义是什么?
  • @ScottHunter 在 limits.h 标头内。我用相同的方式更新了代码
  • 不,不是。 INT_MAX,你反复使用的就是。
  • 这是未定义行为这一事实让编译器在打开优化时将 (x + 1) &gt; x 替换为 true(并且如果 x 是有符号整数类型或具有较低整数类型的整数类型)排名高于int)
  • 因为未定义的行为。

标签: c++ c undefined-behavior


【解决方案1】:

当程序显示undefined behavior 时,C 标准不会预测程序将做什么。程序可能会崩溃,它可能会输出奇怪的结果,或者它可能看起来工作正常。

事实上,编译器通常会在程序不包含未定义行为的假设下工作。

在这个表达式的情况下:

( x+1 ) > x 

鉴于x 具有int 类型,编译器知道有符号溢出是UB,并在它不会发生的假设下工作。考虑到这一点,x 没有值,该表达式可能为假,因此编译器可以优化掉表达式并将其替换为值 1。

当我在 gcc 4.8.5 下运行这个程序时,我得到以下结果:-O0-O1

 2147483647
-2147483648
 0
 2147483647
-2147483648
 0

以下是-O2-O3

 2147483647
-2147483648
 0
 2147483647
-2147483648
 1

然后在后一种情况下查看foo 的程序集:

foo:
.LFB11:
    .file 1 "x1.c"
    .loc 1 4 0
    .cfi_startproc
.LVL0:
    pushq   %rbx                // first call to printf
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    .loc 1 5 0
    movl    %edi, %esi
    .loc 1 4 0
    movl    %edi, %ebx
    .loc 1 5 0
    xorl    %eax, %eax
    movl    $.LC0, %edi
.LVL1:
    call    printf
.LVL2:
    .loc 1 6 0                  // second call to printf
    leal    1(%rbx), %esi
    movl    $.LC0, %edi
    xorl    %eax, %eax
    call    printf
.LVL3:
    .loc 1 8 0                  // return value
    movl    $1, %eax
    popq    %rbx
    .cfi_def_cfa_offset 8
.LVL4:
    ret
    .cfi_endproc

我们可以看到这正是编译器所做的:它优化了比较并且总是返回 1。

这说明了编译器如何利用未定义的行为来应用各种优化。

【讨论】:

  • @avm:其他语言可以做到这一点。它会减慢代码始终检查溢出的速度。 C 不会做你没有要求的额外工作。
  • @avm 注意到未定义行为的开销将是巨大的损失。按照设计,C++ 假定您知道自己在做什么。
  • @avm 它没有检测到 UB。它只是假设它不会发生。这是使 C 快速的部分原因。不引入 UB 取决于程序员。而INT_MAX+1 &gt; INT_MAX UB。你得到你期望的结果这一事实并没有改变这一点。
  • @avm • 未定义的行为 不是编译器问题,而是程序员问题。 C 和 C++ 不是保姆语言。他们假定程序员知道他们在做什么,并且从不对编译器撒谎,并且在他们的代码中永远不会有未定义的行为。在现实世界中,这会转化为大量的错误 工作安全
  • @avm 编译器不需要诊断未定义的行为。他们可能在某些情况下,或者然后可以创建删除某些 UB 实例的扩展,但这种情况通常是不可移植的。
【解决方案2】:

在编写标准时,传统架构的编译器通常会以环绕二进制补码的方式执行整数运算,但有时做其他事情可能更有用。举几个例子:

  1. 如果一个程序已知不会故意导致整数溢出,那么在溢出时设置一个实现陷阱会比让它输出表面上有效但错误的结果要好。

  2. 即使在普通平台上,有时也可以像使用比指定类型更宽的类型一样执行算术运算。例如,在 8086 上,乘法指令将采用两个 16 位操作数并产生 32 位结果,因此在执行像 int32a=int16a*int16b+int32b; 这样的计算时,保持乘法的 32 位结果会比使用符号扩展指令将结果的低 16 位提升为 32 位。此外,该抽象模型将允许简化多种表达式,例如将(x*30/15) 替换为(x*2),或者(如示例所示)将x+y &gt; x 替换为y &gt; 0

与其尝试猜测可能对实现处理整数溢出有用的所有方式,或者冒着阻止实现以客户认为最有用的任何方式处理整数溢出的风险,标准允许实现选择他们认为最有用的任何方法认为最有用。 gcc 的作者认为,处理整数溢出最有用的方法是使用它来产生不受正常因果律约束的扩展推理。

考虑,例如:

unsigned arr[32771];
unsigned mul_mod_32768(unsigned short x, unsigned short y)
{
    /* Note that the authors of the Standard specified that the multiply
       here happens as signed, because--according to the Rationale--they
       expected that commonplace implementations would process signed and
       unsigned math identically in cases like this! */
    return (x * y) & 0x7FFFu;
}
void test(unsigned short n)
{
    unsigned total=0;        
    unsigned short s2=65535;
    for (unsigned short i=32768; i < n; i++)
    {
        total += mul_mod_32768(i, 65535);
    }
    if (n < 32770)
        arr[n] = total;
}

在优化级别 2 或 3 时,gcc 将为 test() 生成代码,该代码完全等同于:

void test(unsigned short n)
{
    arr[n] = 0;
}

如果 n 小于或等于 32768,则循环根本不会运行,total 将为零,total 将存储到 arr[n] 中。如果 n 为 32769,则循环将运行一次,将总数加 0,然后将其存储到 arr[n] 中。如果 n 为 32770 或更大,标准不会强加任何要求,因此 gcc 将像处理其他情况一样处理这些情况,盲目地将零存储到 arr[n]。

该标准故意不尝试禁止专门用于特定狭窄目的的实现以使其不适合许多其他用途的方式行事。这里 gcc 的行为可能适用于仅处理来自可靠来源的数据的程序,但这并不意味着它应该被视为适用于其他任何事情。不幸的是,clang 和 gcc 寻求处理的语言与 C 标准委员会授权描述的语言非常不同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多