GNU GCC (g ++): Dlaczego generuje wielu lekarzy?

90

Środowisko programistyczne: GNU GCC (g ++) 4.1.2

Podczas gdy próbuję zbadać, jak zwiększyć `` pokrycie kodu - szczególnie pokrycie funkcji '' w testach jednostkowych, odkryłem, że niektóre klasy dtor wydają się być generowane wiele razy. Czy ktoś z was ma pojęcie, dlaczego, proszę?

Wypróbowałem i zaobserwowałem to, o czym wspomniałem powyżej, używając następującego kodu.

W „test.h”

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

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

W „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;
}

Kiedy zbudowałem powyższy kod (g ++ test.cpp -o test), a następnie zobaczyłem, jakie symbole zostały wygenerowane w następujący sposób,

nm --demangle test

Widziałem następujący wynik.

==== 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()

Moje pytania są następujące.

1) Dlaczego wygenerowano wielu lekarzy (BaseClass - 2, DerivedClass - 3)?

2) Jaka jest różnica między tymi lekarzami? Jak tych wielu lekarzy będzie selektywnie wykorzystywanych?

Mam teraz wrażenie, że aby osiągnąć 100% pokrycie funkcji projektu C ++, musielibyśmy to zrozumieć, aby móc wywołać wszystkich tych lekarzy w moich testach jednostkowych.

Byłbym bardzo wdzięczny, gdyby ktoś mógł udzielić mi odpowiedzi na powyższe.

Smg
źródło
5
+1 za dołączenie minimalnego, kompletnego programu przykładowego. ( sscce.org )
Robᵩ,
2
Czy twoja klasa bazowa celowo ma niewirtualny destruktor?
Kerrek SB,
2
Mała obserwacja; zgrzeszyłeś i nie uczyniłeś swojego niszczyciela BaseClass wirtualnym.
Lyke,
Przepraszam za moją niekompletną próbkę. Tak, klasa BaseClass powinna mieć wirtualny destruktor, aby te obiekty klas mogły być używane polimorficznie.
Smg,
1
@Lyke: cóż, jeśli wiesz, że nie zamierzasz usunąć elementu pochodnego za pomocą wskaźnika do bazy, to OK, upewniłem się tylko ... zabawne, jeśli sprawisz, że członkowie bazy będą wirtualni, otrzymasz nawet więcej destruktorów.
Kerrek SB,

Odpowiedzi:

74

Po pierwsze, cele tych funkcji są opisane w Itanium C ++ ABI ; zobacz definicje w sekcji „podstawowy destruktor obiektów”, „kompletny niszczyciel obiektów” i „usuwający niszczyciel”. Odwzorowanie na zniekształcone nazwy podano w 5.1.4.

Gruntownie:

  • D2 jest „podstawowym destruktorem obiektów”. Niszczy sam obiekt, a także składowe danych i niewirtualne klasy bazowe.
  • D1 to „kompletny destruktor obiektów”. Dodatkowo niszczy wirtualne klasy bazowe.
  • D0 to „narzędzie do usuwania obiektów”. Robi wszystko, co robi niszczyciel obiektów, a ponadto wywołuje, operator deleteaby faktycznie zwolnić pamięć.

Jeśli nie masz wirtualnych klas podstawowych, D2 i D1 są identyczne; GCC, na wystarczających poziomach optymalizacji, faktycznie aliasuje symbole do tego samego kodu dla obu.

bdonlan
źródło
Dziękuję za jasną odpowiedź. Teraz, z którymi mogę się odnieść, chociaż muszę się więcej uczyć, ponieważ nie jestem zaznajomiony z wirtualnymi dziedzinami.
Smg
@Smg: w dziedziczeniu wirtualnym za odziedziczone klasy „wirtualnie” odpowiada wyłącznie obiekt będący najbardziej pochodnym. To znaczy, jeśli masz, struct B: virtual Aa następnie struct C: B, kiedy niszcząc Binwokujesz, B::D1które z kolei inwokujesz, A::D2a podczas niszczenia Cinwokujesz, C::D1które inwokują B::D2i A::D2(zwróć uwagę, jak B::D2nie wywołuje niszczyciela A). To, co naprawdę jest niesamowite w tym podziale, to faktyczna możliwość zarządzania wszystkimi sytuacjami za pomocą prostej liniowej hierarchii 3 destruktorów.
Matthieu M.
Hmm, może nie zrozumiałem tego jasno ... Myślałem, że w pierwszym przypadku (zniszczenie obiektu B), zamiast A :: D2 zostanie wywołane A :: D1. A także w drugim przypadku (zniszczenie obiektu C), zamiast A :: D2 zostanie wywołany A :: D1. Czy się mylę?
Smg
A :: D1 nie jest wywoływana, ponieważ A nie jest tutaj klasą najwyższego poziomu; odpowiedzialność za zniszczenie wirtualnych klas bazowych A (które mogą istnieć lub nie) nie należy do A, ale raczej do D1 lub D0 klasy najwyższego poziomu.
bdonlan,
37

Zwykle istnieją dwa warianty konstruktora ( nie-odpowiedzialny / odpowiedzialny ) i trzy-destruktor ( nie-odpowiedzialny / odpowiedzialny / odpowiedzialny za usuwanie ).

Nie-in-charge konstruktor i dtor są wykorzystywane podczas pracy obiektu klasy, która dziedziczy z innej klasy za pomocą virtualsłowa kluczowego, gdy obiekt nie znajduje się pełna przedmiot (tak obecny obiekt nie jest „za” konstruowania lub niszczącej wirtualny obiekt bazowy). Ten ctor otrzymuje wskaźnik do wirtualnego obiektu podstawowego i przechowuje go.

W firmę konstruktor i dtors są dla wszystkich innych przypadkach, to znaczy gdy nie ma dziedziczenia wirtualnego zaangażowany; jeśli klasa ma wirtualny destruktor The usuwanie w firmę wskaźnik dtor idzie do vtable gnieździe, natomiast zakres, który zna typ dynamicznego obiektu (czyli dla obiektów z automatycznym lub statyczny okres przechowywania) użyje w firmę dtor (ponieważ ta pamięć nie powinna zostać zwolniona).

Przykład kodu:

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);

Wyniki:

  • Wpis dtor w każdym z vtables do foo, bazi quuxpunktem, w odpowiednim do naładowania znosi dtor.
  • b1i b2są zbudowane przez baz() kierownika , który wzywa foo(1) do zarządzania
  • q1i q2są zbudowane przez osobę quux() odpowiedzialną , która jest foo(2) odpowiedzialna i baz() nie jest odpowiedzialna za pomocą wskaźnika do fooobiektu, który skonstruował wcześniej
  • q2jest niszczony przez ~auto_ptr() in-charge , który wywołuje ~quux() usuwanie wirtualnego dtora odpowiedzialnego za usunięcie , które wywołuje ~baz() nie-odpowiedzialny , ~foo() odpowiedzialny i operator delete.
  • q1jest niszczony przez osobę ~quux() odpowiedzialną , która wywołuje ~baz() brak władzy i ~foo() odpowiedzialność
  • b2jest niszczony przez ~auto_ptr() in-charge , który wywołuje ~baz() usuwanie wirtualnego dtora odpowiedzialnego za usuwanie , które wywołuje ~foo() in-charge ioperator delete
  • b1jest niszczony przez osobę ~baz() odpowiedzialną , która wywołuje ~foo() kontrolę

Każdy, kto wywodzi się z programu, quuxużyłby swojego niebędącego zarządcą i dtora i wziąłby na siebie odpowiedzialność za stworzenie fooobiektu.

W zasadzie wariant nie -odpowiedzialny nigdy nie jest potrzebny dla klasy, która nie ma wirtualnych baz; w takim przypadku wariant odpowiedzialny jest czasami nazywany zunifikowanym i / lub symbole zarówno dla odpowiedzialnego, jak i nie odpowiedzialnego są aliasowane do jednej implementacji.

Simon Richter
źródło
Dziękuję za jasne wyjaśnienie w połączeniu z dość łatwym do zrozumienia przykładem. W przypadku dziedziczenia wirtualnego odpowiedzialność za utworzenie wirtualnego obiektu klasy bazowej spoczywa na klasie pochodnej. Jeśli chodzi o klasy inne niż najbardziej pochodna, mają być one konstruowane przez niezarządzającego konstruktora, aby nie dotykały wirtualnej klasy bazowej.
Smg
Dzięki za krystalicznie jasne wyjaśnienie. Chciałem uzyskać więcej wyjaśnień, co by było, gdybyśmy nie używali auto_ptr i zamiast tego alokowali pamięć w konstruktorze i kasowali w destruktorze. Czy w takim przypadku mielibyśmy tylko dwa destruktory, które nie są odpowiedzialne za usuwanie / usuwanie ich?
nonenone
1
@bhavin, nie, konfiguracja pozostaje dokładnie taka sama. Wygenerowany kod dla destruktora zawsze niszczy sam obiekt i wszystkie podobiekty, więc otrzymujesz kod deletewyrażenia albo jako część własnego destruktora, albo jako część wywołań destruktora podobiektu. deleteEkspresja jest realizowany albo jako wezwanie przez vtable obiektu, jeśli ma destruktor wirtualny (gdzie znajdziemy usunięcia w firmę , lub jako bezpośrednie wywołanie obiektu w firmę destructor.
Simon Richter
deleteWyrażenie nie wywołuje nie-w-charge wariant, który jest używany tylko przez inne destruktorów podczas niszczenia obiektu, który wykorzystuje wirtualny dziedziczenia.
Simon Richter