草庐IT

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

coder 2023-05-02 原文

当我使用 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++ - gcov 报告的析构函数中的分支是什么?的更多相关文章

    1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    2. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    3. ruby - 其他文件中的 Rake 任务 - 2

      我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

    4. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

      作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

    5. ruby-on-rails - Rails 3 中的多个路由文件 - 2

      Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

    6. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

      我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

    7. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    8. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

      我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

    9. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

      我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

    10. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

      为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

    随机推荐