有没有办法调用纯虚拟类的“删除析构函数”?

Posted

技术标签:

【中文标题】有没有办法调用纯虚拟类的“删除析构函数”?【英文标题】:Is there a way to call the "deleting destructor" of a pure virtual class? 【发布时间】:2014-10-29 00:11:35 【问题描述】:

我在 Ubuntu Trusty 上使用 C++11 和 g++4.8。

考虑一下这个sn-p

class Parent 
public:
    virtual ~Parent() =  default;
    virtual void f() = 0;
;

class Child: public Parent 
public:
    void f()
;

调用使用


    Child o;
    o.f();


    Parent * o  = new Child;
    delete o;


    Child * o  = new Child;
    delete o;

我使用 gcov 来生成我的代码覆盖率报告。它报告带有符号_ZN6ParentD0Ev 的析构函数从未被调用,而_ZN6ParentD2Ev 被调用。

Answer Dual emission of constructor symbols 和 GNU GCC (g++): Why does it generate multiple dtors? 报告 _ZN6ParentD0Ev 是删除构造函数。

有没有在Parent类上调用这个“删除析构函数”的情况?

辅助问题:如果没有,有没有办法让 gcov/lcov 代码覆盖工具(在Detailed guide on using gcov with CMake/CDash? 的答案之后使用)在其报告中忽略该符号?

【问题讨论】:

所以答案是“不,没有办法覆盖该功能?” 你有没有想过如何让 gcov 忽略该符号? 如果我没记错的话,我只是忽略了使用标准 GCOV 结构化 cmets 的析构函数的覆盖范围 您是在谈论 LCOV 排除标记吗? ltp.sourceforge.net/coverage/lcov/geninfo.1.php 好的,是的,我能够在派生类周围使用LCOV_EXCL_STARTLCOV_EXCL_STOP 来抑制它。 【参考方案1】:

我认为这是因为您拥有 Child 对象,而不是 Parent 对象。


    Child o;
    o.f();
 // 1


    Parent * o  = new Child;
    delete o;
 // 2


    Child * o  = new Child;
    delete o;
 // 3

// 1中,o被销毁,Child完整对象析构函数被调用。由于Child 继承Parent,它会调用Parent基础对象析构函数,即_ZN6ParentD2Ev

// 2中,动态分配和删除o,调用Childdeleting析构函数。然后,它会调用Parent基础对象析构函数。两者都调用了基础对象析构函数。

// 3 是一样的。它只是等于// 2,除了o 的类型。


我已经在 cygwin & g++ 4.8.3 & windows 7 x86 SP1 上对其进行了测试。这是我的测试代码。

class Parent

public:
    virtual ~Parent()  
    virtual void f() = 0;
;

class Child : public Parent

public:
    void f()  
;

int main()

    
        Child o;
        o.f();
    
    
        Parent * o  = new Child;
        delete o;
    
    
        Child * o  = new Child;
        delete o;
    

和编译 & gcov 选项:

$ g++ -std=c++11 -fprofile-arcs -ftest-coverage -O0 test.cpp -o test
$ ./test
$ gcov -b -f test.cpp

这是结果。

        -:    0:Source:test.cpp
        -:    0:Graph:test.gcno
        -:    0:Data:test.gcda
        -:    0:Runs:1
        -:    0:Programs:1
function _ZN6ParentC2Ev called 2 returned 100% blocks executed 100%
        2:    1:class Parent
        -:    2:
        -:    3:public:
function _ZN6ParentD0Ev called 0 returned 0% blocks executed 0%
function _ZN6ParentD1Ev called 0 returned 0% blocks executed 0%
function _ZN6ParentD2Ev called 3 returned 100% blocks executed 75%
        3:    4:    virtual ~Parent() = default;
call    0 never executed
call    1 never executed
branch  2 never executed
branch  3 never executed
call    4 never executed
branch  5 taken 0% (fallthrough)
branch  6 taken 100%
call    7 never executed
        -:    5:    virtual void f() = 0;
        -:    6:;
        -:    7:
function _ZN5ChildD0Ev called 2 returned 100% blocks executed 100%
function _ZN5ChildD1Ev called 3 returned 100% blocks executed 75%
function _ZN5ChildC1Ev called 2 returned 100% blocks executed 100%
        7:    8:class Child : public Parent
call    0 returned 100%
call    1 returned 100%
call    2 returned 100%
branch  3 taken 0% (fallthrough)
branch  4 taken 100%
call    5 never executed
call    6 returned 100%
        -:    9:
        -:   10:public:
function _ZN5Child1fEv called 1 returned 100% blocks executed 100%
        1:   11:    void f()  
        -:   12:;
        -:   13:
function main called 1 returned 100% blocks executed 100%
        1:   14:int main()
        -:   15:
        -:   16:    
        1:   17:        Child o;
        1:   18:        o.f();
call    0 returned 100%
call    1 returned 100%
        -:   19:    
        -:   20:    
        1:   21:        Parent * o  = new Child;
call    0 returned 100%
call    1 returned 100%
        1:   22:        delete o;
branch  0 taken 100% (fallthrough)
branch  1 taken 0%
call    2 returned 100%
        -:   23:    
        -:   24:    
        1:   25:        Child * o  = new Child;
call    0 returned 100%
call    1 returned 100%
        1:   26:        delete o;
branch  0 taken 100% (fallthrough)
branch  1 taken 0%
call    2 returned 100%
        -:   27:    
        1:   28:

如您所见,_ZN6ParentD2EvBase 的基础对象析构函数被调用,而Base 的其他对象未被调用。

但是,_ZN5ChildD0Ev,删除Child 的析构函数,被调用了两次,_ZN5ChildD1EvChild 的完整对象析构函数,被调用了三次,因为有delete o;Child o;

但是根据我的解释,_ZN5ChildD0Ev 应该被调用两次,_ZN5ChildD1Ev 应该被调用一次,不是吗?为了找出原因,我这样做了:

$ objdump -d test > test.dmp

结果:

00403c88 <__ZN5ChildD0Ev>:
  403c88:   55                      push   %ebp
  403c89:   89 e5                   mov    %esp,%ebp
  403c8b:   83 ec 18                sub    $0x18,%esp
  403c8e:   a1 20 80 40 00          mov    0x408020,%eax
  403c93:   8b 15 24 80 40 00       mov    0x408024,%edx
  403c99:   83 c0 01                add    $0x1,%eax
  403c9c:   83 d2 00                adc    $0x0,%edx
  403c9f:   a3 20 80 40 00          mov    %eax,0x408020
  403ca4:   89 15 24 80 40 00       mov    %edx,0x408024
  403caa:   8b 45 08                mov    0x8(%ebp),%eax
  403cad:   89 04 24                mov    %eax,(%esp)
  403cb0:   e8 47 00 00 00          call   403cfc <__ZN5ChildD1Ev>
  403cb5:   a1 28 80 40 00          mov    0x408028,%eax
  403cba:   8b 15 2c 80 40 00       mov    0x40802c,%edx
  403cc0:   83 c0 01                add    $0x1,%eax
  403cc3:   83 d2 00                adc    $0x0,%edx
  403cc6:   a3 28 80 40 00          mov    %eax,0x408028
  403ccb:   89 15 2c 80 40 00       mov    %edx,0x40802c
  403cd1:   8b 45 08                mov    0x8(%ebp),%eax
  403cd4:   89 04 24                mov    %eax,(%esp)
  403cd7:   e8 a4 f9 ff ff          call   403680 <___wrap__ZdlPv>
  403cdc:   a1 30 80 40 00          mov    0x408030,%eax
  403ce1:   8b 15 34 80 40 00       mov    0x408034,%edx
  403ce7:   83 c0 01                add    $0x1,%eax
  403cea:   83 d2 00                adc    $0x0,%edx
  403ced:   a3 30 80 40 00          mov    %eax,0x408030
  403cf2:   89 15 34 80 40 00       mov    %edx,0x408034
  403cf8:   c9                      leave  
  403cf9:   c3                      ret    
  403cfa:   90                      nop
  403cfb:   90                      nop

是的,因为_ZN5ChildD0Ev 调用了_ZN5ChildD1Ev_ZN5ChildD1Ev 被调用了 3 次。 (1 + 2) 我猜这只是 GCC 的实现 - 用于减少重复。

【讨论】:

这是否意味着在删除对象时,唯一调用的“删除析构函数”是最终/实际类型之一,而不是继承层次结构中的任何其他类型?如果是这样,那么显然它永远不会在Parent 上被调用。【参考方案2】:

你不能有 Parent 对象,所以没有。生成这个不必要的函数是 GCC 的疏忽。优化器确实应该删除它,因为它没有被使用,但我发现 GCC 也有问题。

【讨论】:

【参考方案3】:

正如 ikh 所解释的,当纯虚拟父类具有虚拟析构函数时,D0 析构函数是不必要的生成(并且不可用)。

然而,如果纯虚拟父类有一个非虚拟析构函数,你可以删除一个指向父类型的指针,这调用父类的D0析构函数。当然,父类中的非虚拟析构函数很少是可取的或有意的,因此 g++ 发出警告:[-Wdelete-non-virtual-dtor]

【讨论】:

以上是关于有没有办法调用纯虚拟类的“删除析构函数”?的主要内容,如果未能解决你的问题,请参考以下文章

虚函数本质

如果类有析构函数,堆数组分配 4 个额外字节

为啥纯虚析构函数需要实现

重读STL源码剖析:析构

C++ 虚拟析构函数 (virtual destructor)

析构函数调用线