不明白“假设签名溢出”警告

时间:2012-10-20 02:58:15

标签: c++ gcc gcc-warning

我得到了:

  

警告:假设假设(X + c)< X始终为false [-Wstrict-overflow]

在这一行:

if ( this->m_PositionIndex[in] < this->m_EndIndex[in] ) 

m_PositionIndexm_EndIndex类型itk::Indexhttp://www.itk.org/Doxygen/html/classitk_1_1Index.html),他们的operator[]返回signed long

(这里是第37行:上下文为https://github.com/Kitware/ITK/blob/master/Modules/Core/Common/include/itkImageRegionConstIteratorWithIndex.hxx

有人能解释一下这会引起什么警告吗?我在任何地方都看不到模式(x+c) < x,因为这只是signed long比较。

我尝试在一个自包含的示例中重现它:

#include <iostream>

namespace itk
{
  struct Index
  {
    signed long data[2];

    Index()
    {
      data[0] = 0;
      data[1] = 0;
    }

    signed long& operator[](unsigned int i)
    {
      return data[i];
    }
  };
}
int main (int argc, char *argv[])
{
  itk::Index positionIndex;
  itk::Index endIndex;

  for(unsigned int i = 0; i < 2; i++)
  {
    positionIndex[i]++;
    if ( positionIndex[i] < endIndex[i] )
    {
      std::cout << "something" << std::endl;
    }
  }

  return 0;
}

但我没有收到警告。关于我的演示和真实代码之间有什么不同,或者什么可能导致实际代码中的警告?我通过-Wall标志获得gcc 4.7.0和4.7.2的警告。

6 个答案:

答案 0 :(得分:13)

要简单地停用此警告,请使用-Wno-strict-overflow要禁用触发此警告的特定优化,请使用-fno-strict-overflow-fwrapv

gcc manpage描述了可以使用以下级别控制此警告:-Wstrict-overflow=n

如果由于-Werror导致您停止构建,则可以使用-Wno-error=strict-overflow(或仅-Wno-error覆盖-Werror)来隐藏警告。


分析和评论......

我得到了同样的警告,花了几个小时试图在一个较小的例子中重现它,但从未成功过。我的真正代码涉及在模板化类中调用内联函数,但算法简化为以下...

int X = some_unpredictable_value_well_within_the_range_of_int();
for ( int c=0; c<4; c++ ) assert( X+c >= X ); ## true unless (X+c) overflows

在我的情况下,警告以某种方式与展开for循环的优化器相关联,因此我能够通过声明volatile int c=0来解决问题。修复它的另一件事是声明unsigned int c=0,但我不确定为什么会产生影响。修复它的另一件事是使循环计数足够大以至于循环不会展开,但这不是一个有用的解决方案。

那么这个警告到底在说什么呢?要么说优化器已经修改了算法的语义(假设没有溢出),或者只是告诉你优化器假设你的代码没有未定义的行为溢出有符号整数。除非溢出有符号整数是程序预期行为的一部分,否则此消息可能并不表示代码中存在问题 - 因此您可能希望将其禁用为正常构建。如果您收到此警告但不确定相关代码,则最安全的方法是使用-fwrapv停用优化。

顺便说一下,我在GCC 4.7上遇到了这个问题,但是相同的代码在没有警告的情况下使用4.8编译 - 也许表明GCC开发人员认识到在正常情况下需要稍微“严格”(或者也许这只是因为优化器的差异。)


...哲学

在[C ++ 11:N3242 $ 5.0.4]中,它声明......

  

如果在评估表达式期间,结果不是   在数学上定义或不在可表示值的范围内   它的类型,行为是未定义的。

这意味着符合标准的编译器可以简单地假设从不发生溢出(即使对于无符号类型)。

在C ++中,由于模板和复制构造函数等特性,无意义操作的elision是一个重要的优化器功能。但有时候,如果您使用C ++作为低级“系统语言”,您可能只希望编译器按照您的要求执行操作 - 并依赖底层硬件的行为。鉴于标准的语言,我不知道如何以独立于编译器的方式实现这一目标。

答案 1 :(得分:6)

编译器告诉你它有足够的静态知识,知道如果测试可以优化测试,假设没有签名操作会溢出,那么测试将始终成功。

换句话说,x + 1 < x签名x时唯一的方式是x[-Wstrict-overflow]已经是最大签名值。 {{1}}让编译器在可以假设没有符号加法溢出时发出警告;它基本上告诉你它将优化测试,因为签名溢出是未定义的行为。

如果您想要取消警告,请取消测试。

答案 2 :(得分:1)

我认为编译器改变了

positionIndex[i]++;
if ( positionIndex[i] < endIndex[i] )

进入更优化的东西,如

if ( positionIndex[i]++ < endIndex[i] )

所以

if ( positionIndex[i] + 1 < endIndex[i] )

在溢出的情况下导致未定义的行为(感谢Seth)。

答案 3 :(得分:1)

就我而言,这是编译器的一个很好的警告。我有一个愚蠢的复制/粘贴错误,我在循环中有一个循环,每个都是相同的条件集。像这样:

for (index = 0; index < someLoopLimit; index++)
{
    // blah, blah, blah.....
    for (index = 0; index < someLoopLimit; index++)
    {
        //  More blah, blah, blah....
    }
}

这个警告节省了我调试的时间。谢谢,gcc !!!

答案 4 :(得分:1)

尽管您的问题年代久远,但由于您尚未更改代码的那一部分,因此我认为问题仍然存在,并且您仍然无法对此处的实际情况进行有益的扩展,所以让我试试吧:

在C(和C ++)中,如果将两个SIGNED整数相加会导致溢出,则整个程序运行的行为都是不确定的。因此,执行程序的环境可以执行所需的任何操作(假设存在必要的硬件,则格式化硬盘或启动核战争)。

gcc通常不执行任何操作,但是会执行其他令人讨厌的操作(在不幸的情况下仍可能导致其中之一)。为了说明这一点,让我给您举一个来自Felix von Leitner @ http://ptrace.fefe.de/int.c的简单示例:

#include <assert.h>
#include <stdio.h>

int foo(int a) {
  assert(a+100 > a);
  printf("%d %d\n",a+100,a);
  return a;
}

int main() {
  foo(100);
  foo(0x7fffffff);
}

->我添加了stdio.h,以消除有关未声明printf的警告。

现在,如果运行此命令,我们将期望代码在foo的第二次调用时断言,因为它会创建整数溢出并进行检查。所以,让我们做吧:

$ gcc -O3 int.c -o int && ./int
200 100
-2147483549 2147483647

WTF? (WTF,“ WastäteFefe”的德语-“ Fefe会做什么”,Fefe是Felix von Leitner的昵称,我从中借鉴了代码示例)。哦,顺便说一句,是泰菲?右图:在2007年就此问题编写一个错误报告! https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475

回到您的问题。如果现在尝试进行深入研究,则可以创建一个程序集输出(-S)并仅进行调查以确定assert已被完全删除

$ gcc -S -O3 int.c -o int.s && cat int.s
[...]
foo:
        pushq   %rbx             // save "callee-save" register %rbx
        leal    100(%rdi), %edx  // calc a+100 -> %rdx for printf
        leaq    .LC0(%rip), %rsi // "%d %d\n" for printf
        movl    %edi, %ebx       // save a to %rbx
        movl    %edi, %ecx       // move a to %rcx for printf
        xorl    %eax, %eax       // more prep for printf
        movl    $1, %edi         // and even more prep
        call    __printf_chk@PLT // printf call
        movl    %ebx, %eax       // restore a to %rax as return value
        popq    %rbx             // recover "callee-save" %rbx
        ret                      // and return

在任何地方都没有断言。

现在,让我们在编译期间打开警告。

$ gcc -Wall -O3 int.c -o int.s
int.c: In function 'foo':
int.c:5:2: warning: assuming signed overflow does not occur when assuming that (X + c) >= X is always true [-Wstrict-overflow]
  assert(a+100 > a);
  ^~~~~~

因此,此消息的实际含义是:+ 100可能会溢出,从而导致不确定的行为。因为您是一位技能娴熟的专业软件开发人员,而且从不做任何错事,所以我(gcc)肯定知道,a + 100不会溢出。因为我知道,我也知道a + 100 > a始终是正确的。因为我知道这一点,所以我知道断言永远不会触发。而且因为我知道,我可以消除“消除死代码”优化中的整个断言。

这就是gcc在这里所做的(并警告您)。

现在,在您的小示例中,数据流分析可以确定该整数实际上不会溢出。因此,gcc不需要假定永不溢出,相反,gcc可以证明它永不溢出。在这种情况下,删除代码是绝对可以的(编译器仍然可以在此处警告您删除死代码,但是dce发生的频率很高,以至于没有人想要这些警告)。但是,在您的“真实世界的代码”中,数据流分析失败了,因为并非所有必要的信息都可以进行。因此,gcc不知道a ++是否溢出。因此它会警告您,它假定永远不会发生(然后gcc会删除整个if语句)。

一种解决(或隐藏!!!)问题的方法是在进行a < INT_MAX之前声明a++。无论如何,我担心您那里可能有一些真正的错误,但是我将不得不进行更多的调查才能弄清楚。但是,您可以自己弄清楚,以正确的方式创建MVCE:带有警告的源代码,从包含文件中添加必要的任何内容以使源代码独立(gcc -E有点极端但可以做到),然后开始删除所有不会使警告消失的内容,直到您有了不再删除任何内容的代码为止。

答案 5 :(得分:0)

这几乎肯定是一个错误,即使我不知道它是三个可能的错误中的哪一个。

我相信,gcc在某种程度上弄清楚'this-&gt; m_PositionIndex [in]'等中的值是如何计算出来的,即两个值都来自相同的值,一个带有偏移它也可以被证明是积极的。因此,它认为if / else结构的一个分支是无法访问的代码。

这很糟糕。当然。为什么?嗯,有三种可能性:

  1. gcc是正确的,代码无法访问,你忽略了这种情况。在这种情况下,您只需要臃肿的代码。这三种可能性中最好的,但在代码质量方面仍然很差。

  2. gcc是正确的,因为代码无法访问,但您希望它可以访问。在这种情况下,您需要再次查看无法访问的原因并修复逻辑中的错误。

  3. gcc错了,你偶然发现了一个错误。非常不可能,但很可能,很遗憾很难证明。

  4. 真正糟糕的是,您绝对需要找出代码无法访问的确切原因,或者您需要反驳gcc是否正确(这说起来容易做起来难)。根据墨菲定律,假设案例1或3.没有证明它将确保它是案例2.并且你将不得不第二次寻找这个bug ...