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

Edd*_*onk 39 c++ gcc code-coverage gcov

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

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

int main (int argc, char* argv[])
{
    Foo f;
}
Run Code Online (Sandbox Code Playgroud)

当我运行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'
Run Code Online (Sandbox Code Playgroud)

困扰我的部分是"至少采取一次:50%的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:}
Run Code Online (Sandbox Code Playgroud)

注意"分支0取0%(穿透)"这一行.

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

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

AnT*_*AnT 56

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

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

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

许多编译器按字面意思实现此行为:operator delete从析构函数实现内部直接调用right.当然,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;
} 
Run Code Online (Sandbox Code Playgroud)

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

__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
Run Code Online (Sandbox Code Playgroud)

(析构函数B有点复杂,这就是我A在这里用作例子的原因.但就所涉及的分支而言,析构函数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
Run Code Online (Sandbox Code Playgroud)

请注意我用箭头标记的代码块.这正是我所说的.Register al用作隐藏参数.这个"伪分支"应该operator delete根据值来调用或跳过调用al.但是,在析构函数的第一个版本中,此参数将一如既往地硬编码到正文中0,而在第二个版本中,它将一如既往地硬编码1.

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

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

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

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

当您使用优化启用编译时,GCC将不允许在最终代码中使用诸如无用分支之类的奢侈品.您应该尝试分析优化的代码.非优化的GCC生成的代码有很多无意义的无法访问的分支,就像这个.

  • @Eddy:不要忘记,如果`new`出现在与`delete`相同的范围内,那么编译器可能足够聪明,可以推断出对象的真实动态类型,并将调用虚拟化为析构函数. (2认同)

Ada*_*itz 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>
[...]
Run Code Online (Sandbox Code Playgroud)

  • 天哪!这到底在做什么`je 62 <_ZN3FooD0Ev + 0x62>`?在函数基地址中添加偏移量?: - / (2认同)