编译器没有检测到明显未初始化的变量

Jab*_*cky 59 c

我尝试过的所有C编译器都不会在下面的代码片段中检测到未初始化的变量.但这种情况在这里显而易见.

不要打扰这个代码段的功能.这不是真正的代码,我把它剥离下来调查这个问题.

BOOL NearEqual (int tauxprecis, int max, int value)
{
  int tauxtrouve;      // Not initialized at this point
  int totaldiff;       // Not initialized at this point

  for (int i = 0; i < max; i++)
  {
    if (2 < totaldiff)  // At this point totaldiff is not initialized
    {
      totaldiff = 2;
      tauxtrouve = value;  // Commenting this line out will produce warning
    }
  }

  return tauxtrouve == tauxprecis ;  // At this point tauxtrouve is potentially
                                     // not initialized.
}
Run Code Online (Sandbox Code Playgroud)

另一方面,如果我发表评论tauxtrouve = value ;,我会收到"local variable 'tauxtrouve' used without having been initialized"警告.

我试过这些编译器:

  • GCC 4.9.2 with -Wall -WExtra
  • 启用了所有警告的Microsoft Visual C++ 2013

Bri*_*ain 65

这个变量未初始化的显而易见性被夸大了.路径分析需要花费时间,而您的编译器供应商要么不想实现该功能,要么认为它会花费您太多时间 - 或者您只是没有明确选择加入.

例如,用clang:

$ clang -Wall -Wextra -c obvious.c 
$ clang -Wall -Wextra --analyze -c obvious.c 
obvious.c:9:11: warning: The right operand of '<' is a garbage value
    if (2 < totaldiff)  // at this point totaldiff is not initialized
          ^ ~~~~~~~~~
obvious.c:16:21: warning: The left operand of '==' is a garbage value
  return tauxtrouve == tauxprecis ;  // at this point tauxtrouve is potentially
         ~~~~~~~~~~ ^
2 warnings generated.
Run Code Online (Sandbox Code Playgroud)

这些天真例子的执行时间差异可以忽略不计.但想象一下翻译单元有数千行,数十个函数,每个函数都有循环和重嵌套.路径的数量很快变得复杂并且成为分析是否在该比较之前是否将发生分配的第一次循环的大负担.


编辑:@Matthieu指出,使用LLVM/clang时,由于IR使用的SSA表示法,找到未初始化使用值所需的路径分析不会随着嵌套的增加而复合.

它并不-S -emit-llvm像我希望的那样简单,但我找到了他描述的SSA符号输出.我会说实话,我对LLVM IR不太熟悉,但我会接受Matthieu的话.

底线:clang--analyze某人一起使用,或说服某人修复该gcc错误.

; Function Attrs: nounwind uwtable
define i32 @NearEqual(i32 %tauxprecis, i32 %max, i32 %value) #0 {
  br label %1

; <label>:1                                       ; preds = %7, %0
  %tauxtrouve.0 = phi i32 [ undef, %0 ], [ %tauxtrouve.1, %7 ]
  %i.0 = phi i32 [ 0, %0 ], [ %8, %7 ]
  %2 = icmp slt i32 %i.0, %max
  br i1 %2, label %3, label %9

; <label>:3                                       ; preds = %1
  %4 = icmp slt i32 2, 2
  br i1 %4, label %5, label %6

; <label>:5                                       ; preds = %3
  br label %6

; <label>:6                                       ; preds = %5, %3
  %tauxtrouve.1 = phi i32 [ %value, %5 ], [ %tauxtrouve.0, %3 ]
  br label %7

; <label>:7                                       ; preds = %6
  %8 = add nsw i32 %i.0, 1
  br label %1

; <label>:9                                       ; preds = %1
  %10 = icmp eq i32 %tauxtrouve.0, %tauxprecis
  %11 = zext i1 %10 to i32
  ret i32 %11
}
Run Code Online (Sandbox Code Playgroud)

  • 你有关于退化的数据吗?为了您的信息,LLVM IR基于SSA表示法,顾名思义,它只允许对任何单个变量进行单个赋值(通常,源代码中的变量名称以索引为后缀重复使用).因此,路径必须*具体化,并且检查变量是否可能未初始化使用是非常简单的:对于每个变量,在计算其赋值时,检查它是否已经明确初始化,可能未初始化或绝对未初始化.使用后,请适当警告. (7认同)

hac*_*cks 50

是的,它应该对该未初始化的变量发出警告,但这是一个GCC错误.给出的例子是:

unsigned bmp_iter_set ();
int something (void);

void bitmap_print_value_set (void)
{
    unsigned first;

    for (; bmp_iter_set (); )
    {
        if (!first)
            something ();
        first = 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

并确诊-O2 -W -Wall.

不幸的是,今年是这个bug的10周年!

  • @Lundin你在答案中写道,你"不能指望"编译器检测错误并且"编译器只有两个任务".事实上,这在20年前是正确的,现在不是那么正确.编译器已经有了很大的发展,现在人们依靠编译器来检测明显的错误.尽管存在未定义的行为,但编译器生成警告会很好(并且期望).不是为了让UB定义,而是让程序员知道他做错了.这可能不是*技术*错误,但它肯定是*用户体验错误.* (13认同)
  • Geez ..一个十岁的小虫,有一个评论列表,读起来就像OSS项目中最糟糕的"snark-iness"恐惧的体现 (4认同)
  • 我看到自己将来会读到这篇文章:"..由于*历史原因,这个bug没有得到解决.*" (4认同)
  • @PeterM; 不幸的是,这个10岁的小虫还没有修复! (3认同)
  • #WontFix #NotSexyEnoughToWorryAbout (3认同)

Nat*_*dge 11

这个答案只针对GCC.

经过进一步的调查和评论后,比我之前的答案还要多.此代码段有两个未初始化的变量,并且由于其他原因,每个变量都未被检测到.

首先,该选项的GCC文档-Wuninitialized说:

由于这些警告取决于优化,因此存在警告的确切变量或元素取决于所使用的GCC的精确优化选项和版本.

以前版本的GCC手册更明确地说明了这一点.以下是GCC 3.3.6手册的摘录:

这些警告仅在优化编译时才有可能,因为它们需要仅在优化时计算的数据流信息.如果您未指定-O,则根本不会收到这些警告.

似乎当前版本可能会在没有未初始化变量的情况下给出一些警告-O,但是你仍然会得到更好的结果.

如果我使用编译你的例子gcc -std=c99 -Wall -O,我得到:

foo.c: In function ‘NearEqual’:
foo.c:15:21: warning: ‘tauxtrouve’ is used uninitialized in this function [-Wuninitialized]
   return tauxtrouve == tauxprecis ;  // at this point tauxtrouve is potentially
                     ^
Run Code Online (Sandbox Code Playgroud)

(注意这是使用GCC 4.8.2,因为我没有安装4.9.x,但原则应该是相同的.)

这样可以检测到tauxtrouve未初始化的事实.

但是,如果我们通过为tauxtrouve(但不是for totaldiff)添加初始化程序来部分修复代码,则gcc -std=c99 -Wall -O接受它而不发出任何警告.这似乎是haccks答案中引用的"bug"的一个例子.

关于这是否真的应该被视为一个错误存在一些疑问:GCC不承诺捕获未初始化变量的每个可能实例.实际上,它不能以完美的准确度这样做,因为这是暂停的问题.因此,这样的警告在它们工作时可能会有所帮助,但是没有警告并不能证明您的代码没有未初始化的变量!它们实际上不能代替仔细检查自己的代码.

由haccks链接错误报告中,关于该错误是否可以修复,或者是否尝试检测该特定结构将导致其他正确代码的不可接受的误报率,有很多讨论.