gcov报告的析构函数中的分支是什么?

时间:2011-08-26 02:12:39

标签: c++ gcc code-coverage gcov

当我使用gcov来测量C ++代码的测试覆盖率时,它会在析构函数中报告分支。

struct Foo
{
    virtual ~Foo()
    {
    }
};

int main (int argc, char* argv[])
{
    Foo f;
}

当我启用分支概率启用gcov时(-b)我得到以下输出。

$ gcov /home/epronk/src/lcov-1.9/example/example.gcda -o /home/epronk/src/lcov-1.9/example -b
File 'example.cpp'
Lines executed:100.00% of 6
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Calls executed:40.00% of 5
example.cpp:creating 'example.cpp.gcov'

困扰我的部分是“至少采取一次:50.00%的2”。

生成的.gcov文件提供了更多详细信息。

$ cat example.cpp.gcov | c++filt
        -:    0:Source:example.cpp
        -:    0:Graph:/home/epronk/src/lcov-1.9/example/example.gcno
        -:    0:Data:/home/epronk/src/lcov-1.9/example/example.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:struct Foo
function Foo::Foo() called 1 returned 100% blocks executed 100%
        1:    2:{
function Foo::~Foo() called 1 returned 100% blocks executed 75%
function Foo::~Foo() called 0 returned 0% blocks executed 0%
        1:    3:    virtual ~Foo()
        1:    4:    {
        1:    5:    }
branch  0 taken 0% (fallthrough)
branch  1 taken 100%
call    2 never executed
call    3 never executed
call    4 never executed
        -:    6:};
        -:    7:
function main called 1 returned 100% blocks executed 100%
        1:    8:int main (int argc, char* argv[])
        -:    9:{
        1:   10:    Foo f;
call    0 returned 100%
call    1 returned 100%
        -:   11:}

注意“分支0取0%(穿透)”行。

导致此分支的原因是什么,我需要在代码中做些什么来获得100%?

  • g ++(Ubuntu / Linaro 4.5.2-8ubuntu4)4.5.2
  • gcov(Ubuntu / Linaro 4.5.2-8ubuntu4)4.5.2

3 个答案:

答案 0 :(得分:56)

在典型的实现中,析构函数通常有两个分支:一个用于非动态对象销毁,另一个用于动态对象销毁。通过调用者传递给析构函数的隐藏布尔参数来执行特定分支的选择。它通常通过寄存器传递为0或1。

我猜想,因为在你的情况下,破坏是针对非动态对象的,所以不采用动态分支。尝试添加new - ed,然后添加delete - 类Foo的对象,第二个分支也应该被采用。

这种分支是必要的原因源于C ++语言的规范。当某个类定义自己的operator delete时,选择要调用的特定operator delete就像从类析构函数中查找它一样。最终结果是,对于具有虚拟析构函数operator delete的类,其行为就像是虚拟函数一样(尽管正式成为该类的 static 成员)

许多编译器实现字面这种行为:直接从析构函数实现中调用正确的operator delete。当然,只有在销毁动态分配的对象(不适用于本地或静态对象)时才应调用operator delete。为实现此目的,对operator delete的调用将放入由上述隐藏参数控制的分支中。

在你的例子中,事情看起来很微不足道。我希望优化器能够删除所有不必要的分支。然而,它似乎以某种方式设法在优化中存活下来。


这是一些额外的研究。考虑一下这段代码

#include <stdio.h>

struct A {
  void operator delete(void *) { scanf("11"); }
  virtual ~A() { printf("22"); }
};

struct B : A {
  void operator delete(void *) { scanf("33"); }
  virtual ~B() { printf("44"); }
};

int main() {
  A *a = new B;
  delete a;
} 

在默认优化设置下使用GCC 4.3.4进行编译时,这就是A的析构函数的代码。

__ZN1AD2Ev:                      ; destructor A::~A  
LFB8:
        pushl   %ebp
LCFI8:
        movl    %esp, %ebp
LCFI9:
        subl    $8, %esp
LCFI10:
        movl    8(%ebp), %eax
        movl    $__ZTV1A+8, (%eax)
        movl    $LC1, (%esp)     ; LC1 is "22"
        call    _printf
        movl    $0, %eax         ; <------ Note this
        testb   %al, %al         ; <------ 
        je      L10              ; <------ 
        movl    8(%ebp), %eax    ; <------ 
        movl    %eax, (%esp)     ; <------ 
        call    __ZN1AdlEPv      ; <------ calling `A::operator delete`
L10:
        leave
        ret

B的析构函数有点复杂,这就是我在这里使用A作为例子的原因。但就所涉及的分支而言,{{1}的析构函数以同样的方式做到了。)

但是,在此析构函数之后,生成的代码包含同一个类B 的析构函数的另一个版本,它看起来完全相同的,除了A指令被movl $0, %eax指令替换。

movl $1, %eax

请注意我用箭头标记的代码块。这正是我所说的。注册__ZN1AD0Ev: ; another destructor A::~A LFB10: pushl %ebp LCFI13: movl %esp, %ebp LCFI14: subl $8, %esp LCFI15: movl 8(%ebp), %eax movl $__ZTV1A+8, (%eax) movl $LC1, (%esp) ; LC1 is "22" call _printf movl $1, %eax ; <------ See the difference? testb %al, %al ; <------ je L14 ; <------ movl 8(%ebp), %eax ; <------ movl %eax, (%esp) ; <------ call __ZN1AdlEPv ; <------ calling `A::operator delete` L14: leave ret 用作隐藏参数。这个“伪分支”应该根据al的值调用或跳过对operator delete的调用。但是,在析构函数的第一个版本中,此参数将始终al硬编码到正文中,而在第二个版本中,它始终按照0进行硬编码。

1还为它生成了两个版本的析构函数。因此,我们最终在编译的程序中有4个不同的析构函数:每个类的两个析构函数。

我可以猜测,在开始时,编译器内部就会考虑单个“参数化”析构函数(它与上面描述的中断完全相同)。然后它决定将参数化析构函数拆分为两个独立的非参数化版本:一个用于硬编码参数值B(非动态析构函数),另一个用于硬编码参数值0(动态)析构函数)。在非优化模式下,它通过在函数体内分配实际参数值并使所有分支完全保持完整来实现。我猜这在非优化代码中是可以接受的。这正是你正在处理的问题。

换句话说,你的问题的答案是:在这种情况下,不可能使编译器占用所有分支。没有办法实现100%的覆盖率。其中一些分支是“死的”。在这个版本的GCC中,生成非优化代码的方法相当“懒惰”和“松散”。

我认为可能有一种方法可以阻止非优化模式的拆分。我还没发现它。或者,很可能,它无法完成。较旧版本的GCC使用了真正的参数化析构函数。也许在这个版本的GCC中,他们决定切换到双析构函数方法,并且在执行此操作时,他们以如此快速和肮脏的方式“重用”现有的代码生成器,期望优化器清除无用的分支。

当您使用优化启用编译时,GCC将不会允许自己在最终代码中使用无用的分支等奢侈品。您应该尝试分析优化的代码。非优化的GCC生成的代码有很多无意义的无法访问的分支,如此。

答案 1 :(得分:7)

在析构函数中,GCC为一个永远不会为真的条件生成了一个条件跳转(%al不为零,因为它刚刚被赋值为1):

[...]
  29:   b8 01 00 00 00          mov    $0x1,%eax
  2e:   84 c0                   test   %al,%al
  30:   74 30                   je     62 <_ZN3FooD0Ev+0x62>
[...]

答案 2 :(得分:0)

对于gcc版本5.4.0,仍然存在析构函数问题,但对于Clang来说似乎不存在。

经过测试:

clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

然后使用“llvm-cov gcov ...”生成here所述的覆盖范围。