2016-07-08 42 views
5

(Nota iniziale: questa domanda non è la stessa domanda sulla possibilità o meno di cancellare un puntatore vuoto, sebbene quel problema abbia qualche relazione con il problema identificato nell'aggiornamento 2. La domanda qui è perché una classe base ottiene un valore diverso da this di quello ottenuto dalla classe derivata per lo stesso oggetto.Nel caso in cui l'oggetto derivato chiamerà un metodo suicida della classe base, la classe base deve avere un distruttore virtuale, e il puntatore da eliminare deve essere di tipo pointer-to-class class, memorizzarlo in un void * non è un modo sicuro per eliminare un oggetto da un metodo di classe base.Perché "questo" cambia in parent della classe con più classi base?

Ho un'eredità multipla a forma di diamante dove la classe di mio figlio ha due genitori che ereditano entrambi dallo stesso nonno, quindi:

class Grand 
class Mom : public virtual Grand 
class Dad : public Grand 
class Child : Mom, Dad 

ho scritto Mom e Child, ma Grand e Dad sono classi della libreria non ho scritto (è per questo che Mom eredita praticamente da Grand, ma Dad non lo fa).

Mom implementa un metodo virtuale puro dichiarato in Grand. Dad no. Pertanto, Child implementa anche lo stesso metodo (perché altrimenti il ​​compilatore obietterà che la dichiarazione di tale metodo, Dad, ereditata da Child, non ha avuto implementazione). L'implementazione di Child si limita a chiamare l'implementazione di Mom. Ecco il codice (ho incluso codice per Dad e Grand, come questo è uno SSCCE, non il codice Sono bloccato usando che si basa su classi della libreria non ho scritto):

class Grand 
{ 
public: 
    virtual void WhoAmI(void) = 0; 
}; 

class Mom : public virtual Grand 
{ 
public: 
    virtual void WhoAmI() 
    { 
     void* momThis = this; 
    } 

    //virtual int getZero() = 0; 
}; 

class Dad : public Grand 
{ 
}; 

class Child : Mom, Dad 
{ 
public: 
    void WhoAmI() 
    { 
     void* childThis = this; 
     return Mom::WhoAmI(); 
    } 

    int getZero() 
    { 
     return 0; 
    } 
}; 

int main() 
{ 
    Child* c = new Child; 

    c->WhoAmI(); 
    return 0; 
} 

Si noti che la Il metodo getZero in Child non viene mai chiamato.

Passando attraverso l'esecuzione con il debugger, vedo che l'indirizzo in Child* c è 0x00dcdd08. Entrando in Child::WhoAmI, vedo che l'indirizzo in void* childThis è anche 0x00dcdd08, che è quello che mi aspetto. Avanzando ulteriormente in Mom::WhoAmI, vedo che void* momThis è assegnato a 0x00dcdd0c, che interpreto come l'indirizzo del Mom subobject del mio oggetto moltiplicato ereditato Child (ma ammetto che a questo punto sono un po 'fuori dalla mia profondità).

Va bene, il fatto che Child 's this e Mom' s this sono diversi non mi shock. Ecco cosa succede: se disattivi la dichiarazione di getZero in Mom, e ricomincia da capo, Mom::this e Child::this sono uguali!

Come può l'aggiunta di virtual int getZero() = 0 al risultato Mom classe nel Mom subobject e l'oggetto Child avere lo stesso indirizzo? Ho pensato che forse il compilatore ha riconosciuto che tutti i metodi di Mom erano virtuali e che il suo vtable era lo stesso di Child, quindi in qualche modo sono diventati lo "stesso" oggetto, ma aggiungendo altri e diversi metodi per ogni classe non cambia questo comportamento.

Qualcuno può aiutarmi a capire cosa governa quando this è lo stesso per il genitore e il figlio di un figlio con ereditarietà moltiplicata e quando è diverso?


Aggiornamento

Ho cercato di semplificare le cose per mettere a fuoco come strettamente come posso sulla questione di quando this ha un valore diverso in un oggetto padre di quello che ha in oggetto secondario che dei genitori. Per fare ciò, ho cambiato l'ereditarietà per renderlo un vero diamante, con Dad e Mom entrambi che ereditano virtualmente da Grand. Ho eliminato tutti i metodi virtuali e non è più necessario specificare quale metodo della classe genitore sto chiamando. Invece, ho un metodo unico in ogni classe genitore che mi consentirà di usare il debugger per vedere quale valore ha this in ogni oggetto parent. Quello che vedo è che this è lo stesso per un genitore e il bambino, ma diverso per l'altro genitore. Inoltre, quale genitore ha le diverse modifiche di valore quando l'ordine dei genitori viene modificato nella dichiarazione della classe del bambino.

Questo risulta avere conseguenze catastrofiche se uno degli oggetti padre tenta di cancellarsi. Ecco il codice che, sulla mia macchina, funziona bene:

class Grand 
{ 
}; 

class Mom : public virtual Grand 
{ 
public: 
    void WhosYourMommy() 
    { 
     void* momIam = this; // momIam == 0x0137dd0c 
    } 
}; 

class Dad : public virtual Grand 
{ 
public: 
    void WhosYourDaddy() 
    { 
     void* dadIam = this; // dadIam == 0x0137dd08 
     delete dadIam; // this works 
    } 
}; 

class Child : Dad, Mom 
{ 
public: 
    void WhoAmI() 
    { 
     void* childThis = this; 

     WhosYourMommy(); 
     WhosYourDaddy(); 

     return; 
    } 
}; 

int main() 
{ 
    Child* c = new Child; // c == 0x0137dd08 

    c->WhoAmI(); 

    return 0; 
} 

Tuttavia, se cambio class Child : Dad, Mom a class Child : Mom, Dad, si blocca in fase di esecuzione:

class Grand 
{ 
}; 

class Mom : public virtual Grand 
{ 
public: 
    void WhosYourMommy() 
    { 
     void* momIam = this; // momIam == 0x013bdd08 
    } 
}; 

class Dad : public virtual Grand 
{ 
public: 
    void WhosYourDaddy() 
    { 
     void* dadIam = this; // dadIam == 0x013bdd0c 
     delete dadIam; // this crashes 
    } 
}; 

class Child : Mom, Dad 
{ 
public: 
    void WhoAmI() 
    { 
     void* childThis = this; 

     WhosYourMommy(); 
     WhosYourDaddy(); 

     return; 
    } 
}; 

int main() 
{ 
    Child* c = new Child; // c == 0x013bdd08 

    c->WhoAmI(); 

    return 0; 
} 

Questo è un problema quando si dispone di una classe questo include metodi che possono cancellare oggetti di quella classe (un "metodo suicida"), e quei metodi potrebbero essere chiamati da classi derivate.

Ma, penso di aver trovato la soluzione: qualsiasi classe base che include un metodo che potrebbe eliminare istanze di se stesso e che potrebbero avere quei metodi chiamati da istanze di classi derivate da quella classe deve avere un distruttore virtuale.

Aggiungendo uno al codice di cui sopra rendono l'incidente andare via:

class Grand 
{ 
}; 

class Mom : public virtual Grand 
{ 
public: 
    void WhosYourMommy() 
    { 
     void* momIam = this; // momIam == 0x013bdd08 
    } 
}; 

class Dad : public virtual Grand 
{ 
public: 
    virtual ~Dad() {}; 

    void WhosYourDaddy() 
    { 
     void* dadIam = this; // dadIam == 0x013bdd0c 
     delete dadIam; // this crashes 
    } 
}; 

class Child : Mom, Dad 
{ 
public: 
    void WhoAmI() 
    { 
     void* childThis = this; 

     WhosYourMommy(); 
     WhosYourDaddy(); 

     return; 
    } 
}; 

int main() 
{ 
    Child* c = new Child; // c == 0x013bdd08 

    c->WhoAmI(); 

    return 0; 
} 

Un certo numero di persone che ho incontrato sono atterrito all'idea di un oggetto l'eliminazione di per sé, ma è legale e un idioma necessaria quando si implementa il metodo IUnknown :: Release di COM. Ho trovato good guidelines su come usare delete this in modo sicuro, e alcuni ugualmente good guidelines sull'utilizzo di distruttori virtuali per risolvere questo problema.

Nota, tuttavia, che a meno che la persona che ha codificato la classe genitore lo abbia codificato con un distruttore virtuale, chiamare un metodo di suicidio di tale classe genitore da un'istanza di una classe derivata da quel genitore probabilmente andrà in crash, e fallo in modo imprevedibile. Forse un motivo per includere i distruttori virtuali, anche quando non pensi di averne bisogno.


Update 2

Beh, il problema ritorna se si aggiunge un distruttore virtuale sia DadeMom.Questo codice si blocca quando si tenta di eliminare 'this puntatore s, che non corrisponde Child' Dad s this puntatore:

class Grand 
{ 
}; 

class Mom : public virtual Grand 
{ 
public: 
    virtual ~Mom() {}; 

    void WhosYourMommy() 
    { 
     void* momIam = this; // momIam == 0x013bdd08 
    } 
}; 

class Dad : public virtual Grand 
{ 
public: 
    virtual ~Dad() {}; 

    void WhosYourDaddy() 
    { 
     void* dadIam = this; // dadIam == 0x013bdd0c 
     delete dadIam; // this crashes 
    } 
}; 

class Child : Mom, Dad 
{ 
public: 
    virtual ~Child() {}; 

    void WhoAmI() 
    { 
     void* childThis = this; 

     WhosYourMommy(); 
     WhosYourDaddy(); 

     return; 
    } 
}; 

int main() 
{ 
    Child* c = new Child; // c == 0x013bdd08 

    c->WhoAmI(); 

    return 0; 
} 

Update 3

Grazie a BeyelerStudios per aver posto la domanda giusta: l'eliminazione a void* invece di eliminare un Dad* impedito al C++ di sapere cosa stava effettivamente eliminando e, quindi, impedito di chiamare i distruttori virtuali delle classi base e derivate. La sostituzione di delete dadIam con delete this risolve questo problema e il codice funziona correttamente.

Anche se sarebbe un po 'ridicola, sostituendo delete dadIam con delete (Dad*)dadIam corre anche bene, e aiuta ad illustrare che il tipo del puntatore operato da delete fa la differenza per ciò che delete fa. (Qualcosa che difficilmente troverei sorprendente in un linguaggio polimorfo.)

BeyelerStudios, se vuoi pubblicarlo come risposta, controllerò la casella per te.

Grazie!

+0

Questo è il diamante problema giusto? Presumo che tuo figlio abbia bisogno di cavalcare tutto. Cerca su come risolvere il problema del diamante –

+1

Standard consente al compilatore di inserire la tabella v (che è il modo normale di implementare i tipi polimorfici) ovunque desideri, anche all'inizio della struttura. Quindi non c'è una regola dura e veloce. Essenzialmente è giù per il compilatore. Forse tagga questa domanda con un set di strumenti specifico? – Bathsheba

+0

Probabilmente non correlato, ma si noti che 'Child' ha ancora due genitori' Grand' a causa della presenza di una sola eredità virtuale. – molbdnilo

risposta

0

Come indicato dallo standard [intro.object]:

oggetti possono contenere altri oggetti, detti oggetti secondari. Un subobject può essere [...] un subobject di classe base [...].

Inoltre [expr.prim.this]:

La parola questo nome un puntatore all'oggetto che viene invocata una funzione membro non statica [...].

Va da sé che due diverse classi (derivate e base) sono oggetti diversi, quindi possono avere valori diversi per il puntatore this.

Qualcuno può aiutarmi a capire cosa governa quando questo è lo stesso per il genitore e il figlio di un figlio ereditato da più persone e quando è diverso?

Quando e perché si differenziano non è escluso dalla norma (naturalmente, è soprattutto a causa dell'esistenza di un vtable associata all'oggetto, ma tieni presente che VTables sono semplicemente un modo pratico comune per affrontare il polimorfismo e lo standard non li menziona mai).
Solitamente deriva dall'ABI scelto/implementato (vedere here per ulteriori dettagli su un comune, l'Itanium C++ ABI).

Ne consegue un minimo, ad esempio lavorando a riprodurre la:

#include<iostream> 

struct B { 
    int i; 
    void f() { std::cout << this << std::endl; } 
}; 

struct D: B { 
    void f() { std::cout << this << std::endl; } 
    virtual void g() {} 
}; 

int main() { 
    D d; 
    d.f(); 
    d.B::f(); 
} 

Un'uscita esempio:

0xbef01ac0
0xbef01ac4

Problemi correlati