Есть ли способ вызвать "удаление деструктора" из чистого виртуального класса?

Я использую С++ 11 и g++ 4.8 в Ubuntu Trusty.

Рассмотрим этот фрагмент

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 -.

Ответ Двойное выделение символов конструктора и GNU GCC (g++): Почему он генерирует несколько dtors? сообщает, что _ZN6ParentD0Ev является конструктором удаления.

Есть ли случай, когда этот "удаляющий деструктор" вызывается в классе Parent?

Вспомогательный вопрос: если нет, есть ли способ получить инструмент покрытия кода gcov/lcov (используется следующий ответ Подробное руководство по использованию gcov с CMake/CDash?) игнорировать этот символ в своем отчете?

Ответ 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, он вызывается деструктором базового объекта, который равен _ZN6ParentD2Ev, Parent.

В // 2, o динамически выделяется и удаляется, и вызывается деструктор удаления Child. Затем он вызовет деструктор базового объекта 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:}

Как вы можете видеть, _ZN6ParentD2Ev, деструктор базового объекта Base вызывается, а остальные из Base не вызываются.

Однако, _ZN5ChildD0Ev, удаление деструктора Child, вызывается дважды, а _ZN5ChildD1Ev, полный деструктор объекта Child, вызывается три раза, так как там 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 вызывается три раза. (1 + 2) Я предполагаю, что это просто реализация GCC - для уменьшения дублирования.

Ответ 2

У вас нет родительских объектов, поэтому нет. Это GCC контролирует, что эта ненужная функция генерируется. Оптимизатор действительно должен удалить его, поскольку он не используется, но я обнаружил, что GCC имеет проблемы и в этой области.

Ответ 3

Как пояснил ix, деструктор D0 бесполезно генерируется (и неприменим), когда чистый виртуальный родительский класс имеет виртуальный деструктор.

Однако, если чистый виртуальный родительский класс имеет не виртуальный деструктор, вы можете удалить указатель на родительский тип, и это вызовет родительский деструктор D0. Конечно, не виртуальные деструкторы в родительском классе редко желательны или предназначены, поэтому g++ испускает предупреждение: [-Wdelete-non-virtual-dtor].