Pourquoi est-ce la même chose quand les pointeurs d’object diffèrent en inheritance multiple?

Lors de l’utilisation d’inheritance multiple, C ++ doit gérer plusieurs vtables, ce qui conduit à avoir “plusieurs vues” de classes de base communes.

Voici un extrait de code:

#include "stdafx.h" #include  void dumpPointer( void* pointer ) { __int64 thisPointer = reinterpret_cast( pointer ); char buffer[100]; _i64toa( thisPointer, buffer, 10 ); OutputDebugSsortingngA( buffer ); OutputDebugSsortingngA( "\n" ); } class ICommonBase { public: virtual void Common() = 0 {} }; class IDerived1 : public ICommonBase { }; class IDerived2 : public ICommonBase { }; class CClass : public IDerived1, public IDerived2 { public: virtual void Common() { dumpPointer( this ); } int stuff; }; int _tmain(int argc, _TCHAR* argv[]) { CClass* object = new CClass(); object->Common(); ICommonBase* casted1 = static_cast( static_cast( object ) ); casted1->Common(); dumpPointer( casted1 ); ICommonBase* casted2 = static_cast( static_cast( object ) ); casted2->Common(); dumpPointer( casted2 ); return 0; } 

il produit la sortie suivante:

 206968 //CClass::Common this 206968 //(ICommonBase)IDerived1::Common this 206968 //(ICommonBase)IDerived1* casted1 206968 //(ICommonBase)IDerived2::Common this 206972 //(ICommonBase)IDerived2* casted2 

Ici, casted1 et casted2 ont des valeurs différentes, ce qui est raisonnable, car elles pointent vers des sous-objects différents. Au moment où la fonction virtuelle est appelée, la conversion vers la classe de base a été effectuée et le compilateur ne sait pas qu’il s’agissait à l’origine de la classe la plus dérivée. C’est toujours pareil à chaque fois. Comment ça se passe?

Lorsque l’inheritance multiple est utilisé dans un appel de fonction virtuelle, l’appel de la fonction virtuelle passe souvent à un ‘thunk’ qui ajuste le pointeur this . Dans votre exemple, l’ casted1 pointeur casted1 n’a pas besoin de IDerived1 sous-object CClass de la CClass coïncide avec le début de l’object CClass (ce qui explique pourquoi la valeur du pointeur casted1 est identique à celle du pointeur d’ object CClass ).

Cependant, le pointeur casted2 sur le sous-object IDerived2 ne coïncide pas avec le début de l’object CClass , aussi le pointeur de la fonction vtbl pointe-t-il sur un thunk plutôt que directement sur la fonction CClass::Common() . Le thunk ajuste le pointeur this pour qu’il pointe sur l’object CClass réel, puis passe à la fonction CClass::Common() . Ainsi, il obtiendra toujours un pointeur sur le début de l’object CClass , quel que soit le type de pointeur de sous-object à partir duquel il aurait pu être appelé.

Il existe une très bonne explication à cela dans le livre “Dans le modèle d’object C ++” de S tanley Lippman , Section 4.2 “Fonctions membres virtuelles / Fonctions virtuelles sous MI”.

Lorsqu’ils sont convertis dans un type différent, les décalages de champs ainsi que les entrées de la table virtuelle doivent se trouver dans un emplacement cohérent. Le code qui prend un ICommonBase* tant que paramètre ne sait pas que votre object est vraiment un IDerived2 . Cependant, il devrait toujours pouvoir déréférencer ->foo ou appeler la méthode virtuelle bar() . Si ce ne sont pas à des adresses prévisibles, cela n’a aucun moyen de fonctionner.

Pour le cas d’un inheritance unique, il est facile de faire les choses correctement. Si Derived hérite de Base , vous pouvez simplement dire que le décalage 0 de Derived est également le décalage 0 de Base , et que les membres uniques à Derived peuvent aller après le dernier membre de Base . Pour l’inheritance multiple, cela ne peut évidemment pas fonctionner car le premier octet de Base1 ne peut pas également être le premier octet de Base2 . Chacun a besoin de son propre espace.

Donc, si vous avez une telle classe qui hérite de deux (appelez-la Foo ), le compilateur peut savoir que pour le type Foo , la partie Base1 commence à l’offset X et la partie Base2 à l’offset Y. le compilateur peut simplement append le décalage approprié à this .

Lorsqu’une méthode virtuelle réelle de Foo est appelée, où l’implémentation est fournie par Foo , elle a toujours besoin du pointeur “réel” sur l’object, afin de pouvoir accéder à tous ses membres, et pas seulement à l’instance particulière de Base1 base Base1 Base2 . Par conséquent, this doit toujours indiquer l’object “réel”.

Notez que les détails d’implémentation de ceci peuvent être différents de ceux décrits, il s’agit simplement d’une description détaillée de la raison pour laquelle le problème existe.

Si vous voyez la disposition de l’object en mémoire pour cela, cela ressemblera à ceci:

 v-pointer for IDerived1 v-pointer for IDerived2 .... .... 

Cela peut être autrement aussi .. mais juste pour donner une idée ..

Votre this indiquera toujours le début de l’object, c’est-à-dire où le v-pointer pour IDerived1 est stocké. Toutefois, lorsque vous IDetived2 le pointeur en IDetived2 le pointeur IDetived2 pointe sur le v-pointer pour IDerived2 qui sera décalé de sizeof (pointeur) à partir de this pointeur.

Comme vous l’avez montré dans G ++ 4.3, le même modèle d’object (voir la réponse de Naveen) dit que casted1 et casted2 ont des valeurs différentes.

De plus, dans G ++ 4.3, même si vous utilisez un casting brutal:

 ICommonBase* casted1 = (ICommonBase*)(IDerived1*)object; ICommonBase* casted2 = (ICommonBase*)(IDerived2*)object; 

le résultat est le même.

Très intelligent le compilateur