GNU GCC (g ++): Perché genera più dtor?


90

Ambiente di sviluppo: GNU GCC (g ++) 4.1.2

Mentre sto cercando di indagare su come aumentare la "copertura del codice, in particolare la copertura delle funzioni" nei test unitari, ho scoperto che alcuni dtor di classe sembrano essere generati più volte. Alcuni di voi hanno idea del perché, per favore?

Ho provato e osservato ciò che ho menzionato sopra utilizzando il seguente codice.

In "test.h"

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

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

In "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;
}

Quando ho creato il codice sopra (g ++ test.cpp -o test) e poi ho visto che tipo di simboli sono stati generati come segue,

nm: test di demangle

Ho potuto vedere il seguente output.

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

Le mie domande sono le seguenti.

1) Perché sono stati generati più dtor (BaseClass - 2, DerivedClass - 3)?

2) Quali sono le differenze tra questi medici? Come verranno utilizzati selettivamente quei medici multipli?

Ora ho la sensazione che per ottenere il 100% di copertura delle funzioni per il progetto C ++, avremmo bisogno di capirlo in modo da poter invocare tutti quei medici nei miei unit test.

Apprezzerei molto se qualcuno potesse darmi la risposta sopra.


5
+1 per includere un programma di esempio minimo e completo. ( sscce.org )
Robᵩ

2
La tua classe base ha intenzionalmente un distruttore non virtuale?
Kerrek SB

2
Una piccola osservazione; hai peccato e non hai reso virtuale il tuo distruttore BaseClass.
Lyke

Scusa per il mio campione incompleto. Sì, la BaseClass dovrebbe avere un distruttore virtuale in modo che questi oggetti di classe possano essere utilizzati in modo polimorfico.
Smg

1
@ Lyke: beh, se sai che non cancellerai un derivato tramite un puntatore alla base va bene, stavo solo assicurandomi ... stranamente, se rendi virtuali i membri di base, ottieni anche più distruttori.
Kerrek SB

Risposte:


74

Innanzitutto, gli scopi di queste funzioni sono descritti nell'ABI C ++ Itanium ; vedere le definizioni in "distruttore oggetto di base", "distruttore oggetto completo" e "eliminazione distruttore". La mappatura ai nomi alterati è fornita in 5.1.4.

Fondamentalmente:

  • D2 è il "distruttore dell'oggetto di base". Distrugge l'oggetto stesso, così come i membri dati e le classi di base non virtuali.
  • D1 è il "distruttore di oggetti completo". Inoltre distrugge le classi di base virtuali.
  • D0 è il "distruttore di oggetti per l'eliminazione". Fa tutto ciò che fa il distruttore di oggetti completo, in più chiama operator deleteper liberare effettivamente la memoria.

Se non hai classi di base virtuali, D2 e ​​D1 sono identici; GCC, a livelli di ottimizzazione sufficienti, effettivamente alias i simboli con lo stesso codice per entrambi.


Grazie per la chiara risposta. Ora che posso relazionarmi, anche se ho bisogno di studiare di più perché non ho molta familiarità con il tipo di eredità virtuale.
Smg

@Smg: nell'ereditarietà virtuale, le classi ereditate "virtualmente" sono sotto la sola responsabilità dell'oggetto più derivato. Cioè, se hai struct B: virtual Ae poi struct C: B, quando si distrugge un Bsi richiama B::D1che a sua volta invoca A::D2e quando si distrugge a Csi invoca C::D1quale si invoca B::D2e A::D2(si noti come B::D2non si richiama A distruttore). Quello che veramente stupisce in questa suddivisione è di poter effettivamente gestire tutte le situazioni con una semplice gerarchia lineare di 3 distruttori.
Matthieu M.

Hmm, potrei non aver capito chiaramente il punto ... Ho pensato che nel primo caso (distruggendo l'oggetto B), verrà invocato A :: D1 invece di A :: D2. E anche nel secondo caso (distruzione dell'oggetto C), verrà invocato A :: D1 invece di A :: D2. Ho sbagliato?
Smg

A :: D1 non viene richiamato perché A non è la classe di primo livello qui; la responsabilità di distruggere le classi di base virtuali di A (che possono o non possono esistere) non appartiene ad A, ma piuttosto al D1 o D0 della classe di primo livello.
bdonlan

37

Di solito ci sono due varianti del costruttore ( non in carica / in carica ) e tre del distruttore ( non in carica / in carica / in carica che elimina ).

Il ctor e il dtor non in carica vengono utilizzati quando si gestisce un oggetto di una classe che eredita da un'altra classe utilizzando la virtualparola chiave, quando l'oggetto non è l'oggetto completo (quindi l'oggetto corrente non è "incaricato" di costruire o distruggere l'oggetto base virtuale). Questo ctor riceve un puntatore all'oggetto di base virtuale e lo memorizza.

L' incaricato ctor e dtors sono per tutti gli altri casi, cioè se non v'è alcuna eredità virtuale coinvolti; se la classe ha un distruttore virtuale, il puntatore del dtor di eliminazione in carica va nello slot vtable, mentre uno scope che conosce il tipo dinamico dell'oggetto (cioè per oggetti con durata di memorizzazione automatica o statica) utilizzerà il dtor in carica (perché questa memoria non dovrebbe essere liberata).

Esempio di codice:

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

Risultati:

  • La voce dtor in ciascuno dei VTables per foo, baze quuxil punto in corrispondenza del rispettivo incaricato cancellazione dtor.
  • b1e b2sono costruiti da baz() in carica , che chiama foo(1) in carica
  • q1e q2sono costruiti da quux() un responsabile , che si foo(2) carica e baz() non è in carica con un puntatore fooall'oggetto che ha costruito in precedenza
  • q2viene distrutto da ~auto_ptr() in-carica , che chiama il dtor virtuale ~quux() eliminazione di carica , che chiama ~baz() non incaricato , ~foo() in carica e operator delete.
  • q1viene distrutto da ~quux() in-carica , che chiama ~baz() non-in-carica e ~foo() incaricato
  • b2viene distrutto da ~auto_ptr() in-carica , che chiama il dtor virtuale ~baz() eliminazione di carica , che chiama ~foo() in carica eoperator delete
  • b1viene distrutto da ~baz() in-carica , che chiama ~foo() in carica

Chiunque provenga da quuxutilizzerà il suo ctor e dtor non incaricato e si assumerà la responsabilità della creazione foodell'oggetto.

In linea di principio, la variante gratuita non è mai necessaria per una classe che non ha basi virtuali; in tal caso, la variante in carica viene quindi talvolta chiamata unificata e / o i simboli sia per incaricato che per non incaricato sono alias per una singola implementazione.


Grazie per la tua chiara spiegazione in combinazione con un esempio abbastanza facile da capire. Nel caso in cui sia coinvolta l'ereditarietà virtuale, è responsabilità della classe più derivata creare un oggetto di classe base virtuale. Per quanto riguarda le altre classi rispetto alla classe più derivata, dovrebbero essere interpretate da un costruttore non in carica in modo che non tocchino la classe di base virtuale.
Smg

Grazie per la chiara spiegazione. Volevo ottenere chiarimenti su più cose, e se non usassimo auto_ptr e invece allocassimo la memoria nel costruttore e cancellassimo nel distruttore. In tal caso avremmo solo due distruttori non in carica / in carica che cancellano?
nonenone

1
@bhavin, no, la configurazione rimane esattamente la stessa. Il codice generato per un distruttore distrugge sempre l'oggetto stesso e qualsiasi sotto-oggetto, quindi ottieni il codice per l' deleteespressione come parte del tuo distruttore o come parte delle chiamate del distruttore sotto-oggetto. L' deleteespressione è implementata o come una chiamata attraverso la vtable dell'oggetto se ha un distruttore virtuale (dove troviamo l' eliminazione in carica , o come una chiamata diretta al distruttore in carica dell'oggetto .
Simon Richter

deleteUn'espressione mai chiama il non-in-carica variante, che viene utilizzato solo dagli altri distruttori distruggendo un oggetto che utilizza l'ereditarietà virtuale.
Simon Richter
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.