GNU GCC (g ++): почему он генерирует несколько dtors?

91

Среда разработки: 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) Чем отличаются эти дторы? Как эти множественные дторы будут использоваться выборочно?

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

Я был бы очень признателен, если бы кто-нибудь мог дать мне ответ на вышеизложенное.

Smg
источник
5
+1 за включение минимальной, полной программы-примера. ( sscce.org )
Robᵩ 07
2
У вашего базового класса намеренно есть не виртуальный деструктор?
Kerrek SB 07
2
Небольшое наблюдение; вы согрешили и не сделали виртуальный деструктор BaseClass.
Lyke
Извините за неполный образец. Да, у BaseClass должен быть виртуальный деструктор, чтобы эти объекты класса можно было использовать полиморфно.
Smg 07
1
@Lyke: ну, если вы знаете, что не собираетесь удалять производные через указатель на базу, это нормально, я просто удостоверился ... как ни странно, если вы сделаете базовые члены виртуальными, вы получите даже больше деструкторов.
Kerrek SB 07

Ответы:

75

Во-первых, назначение этих функций описано в Itanium C ++ ABI ; см. определения в разделах «деструктор базового объекта», «полный деструктор объекта» и «деструктор удаления». Сопоставление с искаженными именами приведено в 5.1.4.

В принципе:

  • D2 - это «деструктор базового объекта». Он уничтожает сам объект, а также члены данных и невиртуальные базовые классы.
  • D1 - это «полный деструктор объекта». Он дополнительно уничтожает виртуальные базовые классы.
  • D0 - это «деструктор удаления объекта». Он делает все, что делает полный деструктор объекта, плюс вызывает operator deleteфактическое освобождение памяти.

Если у вас нет виртуальных базовых классов, D2 и D1 идентичны; GCC при достаточных уровнях оптимизации фактически присваивает символы одному и тому же коду для обоих.

бдонлан
источник
Спасибо за четкий ответ. Теперь, когда я могу относиться к этому, хотя мне нужно больше изучать, поскольку я не так хорошо знаком с вещами типа виртуального наследования.
Smg 07
@Smg: при виртуальном наследовании «виртуально» унаследованные классы находятся под исключительной ответственностью самого производного объекта. То есть, если у вас есть struct B: virtual Aand then struct C: B, то при уничтожении Bвы вызываете, B::D1который по очереди вызывает, A::D2а при уничтожении Cвы вызываете, C::D1который вызывает B::D2и A::D2(обратите внимание, как B::D2не вызывает деструктор A). Что действительно удивительно в этом подразделении, так это возможность управлять всеми ситуациями с помощью простой линейной иерархии из 3 деструкторов.
Matthieu M.
Хм, возможно, я не совсем понял суть ... Я думал, что в первом случае (уничтожение объекта B) будет вызываться A :: D1 вместо A :: D2. А также во втором случае (при уничтожении объекта C) вместо A :: D2 будет вызываться A :: D1. Я ошибся?
Smg 08
A :: D1 не вызывается, потому что A здесь не класс верхнего уровня; ответственность за уничтожение виртуальных базовых классов A (которые могут существовать, а могут и не существовать) принадлежит не A, а скорее D1 или D0 класса верхнего уровня.
bdonlan 08
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() in-charge , которое вызывает удаление виртуального dtor- ~quux() in-charge , которое вызывает ~baz() not-in-charge , ~foo() in-charge и operator delete.
  • q1разрушается ~quux() ответственным , который вызывает ~baz() незаправленные и ~foo() ответственные
  • b2уничтожается ~auto_ptr() ответственным , который вызывает ~baz() удаление ответственного виртуального dtor , которое вызывает ~foo() ответственный иoperator delete
  • b1разрушаются путем ~baz() в заряде , который вызывает ~foo() в заряду

Любой, кто производный от, quuxбудет использовать его не отвечающий за ctor и dtor и взять на себя ответственность за создание fooобъекта.

В принципе, бесплатный вариант никогда не нужен для класса, у которого нет виртуальных баз; в этом случае, в заряде вариант , то иногда называют унифицирован , и / или символы для обоего в заряде и не-в-заряде являются псевдонимами к одной реализации.

Саймон Рихтер
источник
Спасибо за четкое объяснение в сочетании с довольно простым для понимания примером. В случае, если задействовано виртуальное наследование, создание объекта виртуального базового класса является обязанностью самого производного класса. Что касается других классов, кроме самого производного, они должны быть созданы не отвечающим за них конструктором, чтобы они не касались виртуального базового класса.
Smg 08
Спасибо за кристально ясное объяснение. Я хотел получить пояснение по поводу того, что, если мы не будем использовать auto_ptr, а вместо этого выделим память в конструкторе и удалим в деструкторе. В таком случае у нас будет только два деструктора, не отвечающих / удаляющих ответственных?
nonenone
1
@bhavin, нет, настройки остались прежними. Сгенерированный код деструктора всегда уничтожает сам объект и любые подобъекты, поэтому вы получаете код deleteвыражения либо как часть вашего собственного деструктора, либо как часть вызовов деструктора подобъекта. deleteВыражение реализуется либо как вызов через таблицу виртуальных объекта , если он имеет виртуальный деструктор (где мы находим в заряду удаление , или как прямой вызов объекта в обязанности деструктор.
Simon Richter
deleteВыражение никогда не называет не-в-заряда вариант, который используется только другими деструкторов, уничтожая объект , который использует виртуальное наследование.
Саймон Рихтер