(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 Dad
eMom
.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!
Questo è il diamante problema giusto? Presumo che tuo figlio abbia bisogno di cavalcare tutto. Cerca su come risolvere il problema del diamante –
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
Probabilmente non correlato, ma si noti che 'Child' ha ancora due genitori' Grand' a causa della presenza di una sola eredità virtuale. – molbdnilo