GNU GCC (g ++): Чому він генерує кілька dtors?


90

Розвиваюче середовище: GNU GCC (g ++) 4.1.2

Поки я намагаюся дослідити, як збільшити "охоплення коду - зокрема, функціональне покриття" в модульному тестуванні, я виявив, що частина класу dtor, здається, генерується кілька разів. Хтось із вас уявляє, чому, будь ласка?

Я спробував і спостерігав те, що я згадав вище, використовуючи наступний код.

У "test.h"

class BaseClass
{
public:
    ~BaseClass();
    void someMethod();
};

class DerivedClass : public BaseClass
{
public:
    virtual ~DerivedClass();
    virtual void someMethod();
};

У "test.cpp"

#include <iostream>
#include "test.h"

BaseClass::~BaseClass()
{
    std::cout << "BaseClass dtor invoked" << std::endl;
}

void BaseClass::someMethod()
{
    std::cout << "Base class method" << std::endl;
}

DerivedClass::~DerivedClass()
{
    std::cout << "DerivedClass dtor invoked" << std::endl;
}

void DerivedClass::someMethod()
{
    std::cout << "Derived class method" << std::endl;
}

int main()
{
    BaseClass* b_ptr = new BaseClass;
    b_ptr->someMethod();
    delete b_ptr;
}

Коли я створив наведений вище код (g ++ test.cpp -o test), а потім побачив, які символи були сформовані наступним чином,

nm - тест демангле

Я міг бачити наступний результат.

==== following is partial output ====
08048816 T DerivedClass::someMethod()
08048922 T DerivedClass::~DerivedClass()
080489aa T DerivedClass::~DerivedClass()
08048a32 T DerivedClass::~DerivedClass()
08048842 T BaseClass::someMethod()
0804886e T BaseClass::~BaseClass()
080488f6 T BaseClass::~BaseClass()

Мої запитання такі.

1) Чому було створено кілька dtors (BaseClass - 2, DerivedClass - 3)?

2) Яка різниця між цими dtors? Як ці множинні двигуни будуть використовуватися вибірково?

Зараз у мене відчуття, що для досягнення 100% покриття функцій для проекту C ++ нам потрібно це зрозуміти, щоб я міг викликати всі ці dtors у своїх модульних тестах.

Буду дуже вдячний, якщо хтось зможе дати мені відповідь на вищезазначене.


5
+1 за включення мінімальної, повної зразкової програми. ( sscce.org )
Робᵩ

2
У вашому базовому класі навмисно є невіртуальний деструктор?
Kerrek SB

2
Невелике спостереження; ви згрішили і не зробили ваш деструктор BaseClass віртуальним.
Lyke

Вибачте за мій неповний зразок. Так, BaseClass повинен мати віртуальний деструктор, щоб ці об'єкти класу могли використовуватися поліморфно.
Smg

1
@Lyke: ну, якщо ти знаєш, що не збираєшся видаляти похідне через вказівник на базу, це нормально, я просто переконався ... смішно, якщо ти зробиш базових членів віртуальними, ти отримаєш навіть більше деструкторів.
Kerrek SB

Відповіді:


74

Спочатку цілі цих функцій описані в Itanium C ++ ABI ; див. визначення у розділі "базовий деструктор об'єкта", "повний деструктор об'єкта" та "видалення деструктора". Зіставлення зі спотвореними іменами наведено в 5.1.4.

В основному:

  • D2 - "деструктор базового об'єкта". Він знищує сам об'єкт, а також члени даних та невіртуальні базові класи.
  • D1 - це "повний деструктор об'єктів". Це додатково знищує віртуальні базові класи.
  • D0 - це "деструктор видалення об'єкта". Він робить все, що робить повний деструктор об’єктів, плюс закликає operator deleteфактично звільнити пам’ять.

Якщо у вас немає віртуальних базових класів, D2 і D1 ідентичні; GCC, на достатніх рівнях оптимізації, фактично псевдоніми символів до одного і того ж коду для обох.


Дякую за чітку відповідь. Тепер я можу зв’язатися з цим, хоча мені потрібно вчитися більше, оскільки я не настільки знайомий з видами віртуального успадкування.
Smg

@Smg: у віртуальному успадкуванні "віртуально" успадковані класи знаходяться під єдиною відповідальністю найбільш похідного об'єкта. Тобто, якщо у вас є struct B: virtual Aі потім struct C: B, то при знищенні Bви викликаєте, B::D1яке по черзі викликає, A::D2а при знищенні Cви викликаєте, C::D1яке викликає B::D2і A::D2(зверніть увагу, як B::D2не викликає деструктор). Що справді дивно в цьому підрозділі, так це те, що насправді можна управляти всіма ситуаціями за допомогою простої лінійної ієрархії з 3 деструкторів.
Matthieu M.

Хм, я, можливо, не зрозумів суті чітко ... Я думав, що в першому випадку (руйнуючи об'єкт B) замість A :: D2 буде викликано A :: D1. А також у другому випадку (руйнуючи C-об'єкт), A :: D1 буде викликано замість A :: D2. Я помиляюся?
Smg

A :: D1 не викликається, оскільки A тут не є класом верхнього рівня; відповідальність за знищення віртуальних базових класів A (які можуть існувати або не існувати) не належить A, а скоріше D1 або D0 класу верхнього рівня.
bdonlan

37

Зазвичай є два варіанти конструктора ( не-зарядний / зарядний ) і три деструктора ( не-зарядний / зарядний / зарядний видалення ).

Чи не в заряді т х р і dtor використовуються при роботі з об'єктом класу , який успадковує від іншого класу , використовуючи virtualключове слово, коли об'єкт не є закінченим об'єктом (так поточний об'єкт «не відповідає» побудова або руйнівній об'єкт віртуальної бази). Цей ctor отримує вказівник на віртуальний базовий об'єкт і зберігає його.

У заряді т е р і dtors є для всіх інших випадків, тобто , якщо немає віртуального успадкування бере участь; якщо у класі є віртуальний деструктор, покажчик dtor, що видаляє заряд, переходить у слот vtable, тоді як область, яка знає динамічний тип об'єкта (тобто для об'єктів з автоматичним або статичним тривалістю зберігання), використовуватиме заряджений dtor (оскільки ця пам’ять не повинна звільнятися).

Приклад коду:

struct foo {
    foo(int);
    virtual ~foo(void);
    int bar;
};

struct baz : virtual foo {
    baz(void);
    virtual ~baz(void);
};

struct quux : baz {
    quux(void);
    virtual ~quux(void);
};

foo::foo(int i) { bar = i; }
foo::~foo(void) { return; }

baz::baz(void) : foo(1) { return; }
baz::~baz(void) { return; }

quux::quux(void) : foo(2), baz() { return; }
quux::~quux(void) { return; }

baz b1;
std::auto_ptr<foo> b2(new baz);
quux q1;
std::auto_ptr<foo> q2(new quux);

Результати:

  • Запис dtor у кожній з таблиць, що вказує на foo, bazі quuxвказує на відповідний відповідальний за видалення dtor.
  • b1і b2будуються за baz() платою , яка викликає foo(1) плату
  • q1і q2будуються quux() зарядкою , яка падає foo(2) за заряд і baz() заряд за допомогою вказівника на fooпобудований раніше об'єкт
  • q2руйнуються шляхом ~auto_ptr() в заряді , який викликає віртуальний dtor ~quux() в заряду видалення , в якому міститься заклик ~baz() не в обов'язки , ~foo() в обов'язки і operator delete.
  • q1руйнується з ~quux() -за плати , яка дзвонить ~baz() не за заряд і ~foo() заряд
  • b2руйнується ~auto_ptr() in-charge , який викликає віртуальний dtor ~baz() у видаленні зарядного , який викликає ~foo() in-charge іoperator delete
  • b1руйнується ~baz() зарядником , який викликає ~foo() заряд

Будь-які випливають з quuxвикористовуватиме його НЕ-в-заряду CTOR і dtor і взяти на себе відповідальність за створення fooоб'єкта.

В принципі, безкоштовний варіант ніколи не потрібен для класу, який не має віртуальних баз; у такому випадку варіант, що відповідає за виконання , іноді називають уніфікованим , і / або символи як для відповідального, так і для невиконавчого відповідають псевдонімам однієї реалізації.


Дякуємо за чітке пояснення у поєднанні з досить простим для розуміння прикладом. У тому випадку, якщо йдеться про віртуальне успадкування, створення віртуального базового об'єкта класу несе відповідальність найбільш похідного класу. Що стосується інших класів, крім самого похідного класу, вони, як передбачається, тлумачаться не зарядним конструктором, щоб вони не торкалися віртуального базового класу.
Smg

Дякую за кришталево чітке пояснення. Я хотів отримати роз'яснення щодо іншого, що, якщо ми не використовуємо auto_ptr, а замість цього виділяємо пам'ять у конструкторі та видаляємо в деструкторі. У такому випадку ми мали б лише два деструктори, які не видаляють головне / відповідальне за видалення?
nonenone

1
@bhavin, ні, установка залишається точно такою ж. Створений код для деструктора завжди знищує сам об'єкт та будь-які під-об'єкти, тому ви отримуєте код для deleteвиразу або як частину власного деструктора, або як частину викликів деструктора під-об'єкта. deleteВираз реалізується або як виклик через таблицю віртуальних об'єкта , якщо він має віртуальний деструктор (де ми знаходимо в заряду видалення , або як прямий виклик об'єкта в обов'язки деструктор.
Simon Richter

deleteВираз ніколи не називає НЕ-в-заряду варіант, який використовується тільки іншими деструкторов, знищуючи об'єкт , який використовує віртуальне успадкування.
Саймон Ріхтер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.