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

标签 c++ gcc code-coverage gcov

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

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

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

当我在启用分支概率 (-b) 的情况下运行 gcov 时,我得到以下输出。
$ 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'

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

生成的 .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:}

注意“branch 0 take 0% (fallthrough)”这一行。

是什么导致了这个分支,我需要在代码中做什么才能在这里得到 100%?
  • g++ (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
  • gcov (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
  • 最佳答案

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

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

    这种分支的必要性源于 C++ 语言的规范。当某个类定义了自己的 operator delete ,具体选择operator delete调用就像从类析构函数内部查找一样完成。最终结果是对于具有虚拟析构函数的类 operator delete表现得好像它是一个虚函数(尽管正式是类的静态成员)。

    许多编译器从字面上实现了这种行为:正确的 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;
    } 
    

    这就是 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
    

    (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
    

    请注意我用箭头标记的代码块。这正是我所说的。注册 al作为隐藏参数。这个“伪分支”应该调用或跳过对 operator delete 的调用。按照al的值.然而,在析构函数的第一个版本中,这个参数像往常一样被硬编码到主体中 0 ,而在第二个中,它一如既往地硬编码 1 .

    类(class)B还为它生成了两个版本的析构函数。所以我们最终在编译的程序中有 4 个不同的析构函数:每个类有两个析构函数。

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

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

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

    当您在启用优化的情况下进行编译时,GCC 将不允许自己在最终代码中使用无用的分支等奢侈品。您可能应该尝试分析优化的代码。非优化 GCC 生成的代码有很多像这样的无意义的无法访问的分支。

    关于c++ - gcov 报告的析构函数中的分支是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7199360/

    相关文章:

    c++ - 自定义比较器—— "<="会比 "<"节省交换吗?

    c++ - 构造函数重载造成困惑

    c++ - 如何在不使用键或同时关闭的情况下更新 cv::namedWindow

    c++ - 命令行上的 vcpkg 设置错误

    c - MinGW 内联汇编中的 "Error: backward ref to unknown label..."

    c - 查找 popen 输出更改

    c++ - 这是什么 C++ 语言构造 : # (i. e。哈希)整数 "path_to_header_or_cpp_file"<整数>?

    ios - 单元测试策略,理想的代码覆盖基线

    python - 如何在单个 html 文件中获取 django 覆盖率报告

    c# - MS 测试代码覆盖率返回不正确的结果