2008-11-18 12 views
6

Le réseau déborde d'explications du "dreaded diamond problem". Tout comme StackOverflow. Je pense que je comprends un peu, mais je n'arrive pas à traduire ces connaissances en comprenant quelque chose de similaire et de différent.Héritage virtuel multiple C++ par rapport à COM

Ma question commence comme une pure question C++, mais la réponse pourrait bien se ramifier dans les spécificités MS-COM. La question du problème général va:

class Base { /* pure virtual stuff */ }; 
class Der1 : Base /* Non-virtual! */ { /* pure virtual stuff */ }; 
class Der2 : Base /* Non-virtual! */ { /* pure virtual stuff */ }; 
class Join : virtual Der1, virtual Der2 { /* implementation stuff */ }; 
class Join2 : Join { /* more implementation stuff + overides */ }; 

C'est pas la solution classique du diamant. Exactement ce que "virtuel" fait ici?

Mon véritable problème est d'essayer de comprendre un discussion over at our friends' place at CodeProject. Il s'agit d'une classe personnalisée pour créer un conteneur transparent pour le lecteur Flash. J'ai pensé que j'essaierais cet endroit pour s'amuser. Il s'avère que la déclaration suivante plante votre application, avec la version 10 du lecteur Flash.

class FlashContainerWnd: virtual public IOleClientSite, 
          virtual public IOleInPlaceSiteWindowless, 
          virtual public IOleInPlaceFrame, 
          virtual public IStorage 

Debugging montre que lors de la saisie des implémentations de fonction (QueryInterface etc.), à partir de différents appelants, je reçois différents « ce » valeurs -pointer pour les appels différents. Mais enlever "virtuel" fait l'affaire! Pas de plantage, et même "this" -pointer.

Je voudrais bien comprendre exactement ce qui se passe. Merci beaucoup.

Vive Adam

+0

Je ne suis pas bon en héritage virtuel. mais est-ce que votre application contient une distribution quelque part qui passe de IOle ou IStorage à FlashContainerWnd? –

Répondre

2

Je pense la question avec votre exemple COM est qu'en ajoutant le mot-clé virtuel, vous dites que tous les Iole * interfaces partagent une implémentation IUnknown commune. Pour implémenter cela, le compilateur doit créer plusieurs tables v, d'où différentes valeurs 'this' en fonction de la classe dérivée.

COM exige que lorsque vous appelez IQueryInterface sur un objet pour IUnknown que TOUS interfaces exposées par l'objet renvoient le même IUnknown ... qui cette mise en œuvre rompt clairement.

Sans l'héritage virtuel, chaque IOle * a nominalement sa propre implémentation IUnknown. Cependant, étant donné que IUnknown est une classe abstraite, et qu'il n'y a pas de stockage dans le compilateur, et que toutes les implémentations IUnknown proviennent de FlashContainerWnd, il n'y a qu'une seule implémentation.

(OK, ce dernier peu semble faible ... peut-être quelqu'un avec une meilleure compréhension des règles linguistiques peut l'expliquer plus clairement)

+0

Oui, je pense que vous avez raison. Vous obtenez le IUnknown, puis utilisez QueryInterface pour obtenir l'interface que vous voulez. Vous ne lancez pas un objet COM de la manière habituelle en C++. – gbjbaanb

+0

Merci, si je vous comprends bien, je devrais utiliser "virtuel", mais le fait est que je dois l'enlever, pour arrêter le crash. Donc, je ne comprends pas comment cela peut poser ma question. – Adam

3

L'héritage virtuel dans le premier exemple ne faites rien. Je parierais qu'ils compilent au même code s'ils ont été supprimés.

La classe à héritage virtuel indique simplement au compilateur qu'il doit fusionner les versions ultérieures de Der1 ou Der2. Puisqu'un seul de chaque apparaît dans l'arbre d'héritage, rien n'est fait. Les virtual n'ont aucun effet sur Base.L'héritage virtuel affecte uniquement la classe héritée suivante, et uniquement pour les instances qui ont été supprimées virtuelles. C'est en arrière de ce que vous attendez, mais c'est une limitation sur la façon dont les classes sont compilées.

class A {}; 
class B : virtual public A {}; 
class C : virtual public A {}; 
class D : public A {}; 
class E : virtual public A, public B, public C, public D {}; 
class F : public A, public B, public C, public D {}; 

F::A != F::B::A or F::C::A or F::D::A 
F::B::A == F::C::A 
F::D::A != F::B::A or F::C::A or F::A 

E::B::A == E::C::A == E::A 
E::D::A != E::B::A or E::C::A or E::D::A 

L'une des raisons A doit être marqué au lieu virtuel en C et B de E ou F est que C et B doivent savoir ne pas appeler le constructeur de A. Normalement, ils auraient initialisé chacune de leurs copies. Quand ils sont impliqués dans l'héritage du diamant, ils ne le feront pas. Mais vous ne pouvez pas recompiler B et C pour ne pas construire A. Cela signifie que C et B doivent savoir à l'avance pour créer un code constructeur où le constructeur de A n'est pas appelé.

+0

Je ne suis pas sûr de ce que vous entendez par "instances qui ont été supprimées virtuelles". Si vous voulez dire l'héritage virtuel subséquent, alors mon problème du monde réel, malheureusement, prouve votre réponse fausse. Parce que je n'ai que virtuel dans un seul niveau. Avec virtuel: crash Sans virtuel: bien. – Adam

0

C'est un peu daté maintenant, mais la meilleure référence que j'ai jamais rencontrée qui concerne les internes C++ est le modèle d'objet C++ de Lippman. Les détails de l'implémentation exacte peuvent ne pas correspondre à la sortie de votre compilateur, mais la compréhension qu'il fournit est extrêmement précieuse. Autour de la page 96, il y a une explication de l'héritage virtuel et elle aborde spécifiquement le problème du diamant.

Je vais vous laisser lire les détails, mais fondamentalement l'utilisation de l'héritage virtuel nécessite une recherche dans la table virtuelle afin de localiser la classe de base. Ce n'est pas le cas dans l'héritage normal, où l'emplacement de la classe de base peut être calculé au moment de la compilation.

(La dernière fois que je pris la voie facile et juste recommandé un livre pour répondre à une question de débordement de pile je me suis voté considérablement, donc nous allons voir si cela se produit à nouveau ... :)

+0

Merci, mais ce n'est * pas * le problème du diamant, si vous lisez attentivement ma question. – Adam

0

Je pensais que je J'essaierais juste ton exemple. Je suis venu avec:

#include "stdafx.h" 
#include <stdio.h> 

class Base 
{ 
public: 
    virtual void say_hi(const char* s)=0; 
}; 

class Der1 : public Base 
{ 
public: 
    virtual void d1()=0; 
}; 

class Der2 : public Base 
{ 
public: 
    virtual void d2()=0; 
}; 

class Join : virtual public Der1, virtual public Der2 
      // class Join : public Der1, public Der2 
{ 
public: 
    virtual void say_hi(const char* s); 
    virtual void d1(); 
    virtual void d2(); 
}; 

class Join2 : public Join 
{ 
    virtual void d1(); 
}; 

void Join::say_hi(const char* s) 
{ 
    printf("Hi %s (%p)\n", s, this); 
} 

void Join::d1() 
{} 

void Join::d2() 
{} 

void Join2::d1() 
{ 
} 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    Join2* j2 = new Join2(); 
    Join* j = dynamic_cast<Join*>(j2); 
    Der1* d1 = dynamic_cast<Der1*>(j2); 
    Der2* d2 = dynamic_cast<Der2*>(j2); 
    Base* b1 = dynamic_cast<Base*>(d1); 
    Base* b2 = dynamic_cast<Base*>(d2); 

    printf("j2: %p\n", j2); 
    printf("j: %p\n", j); 
    printf("d1: %p\n", d1); 
    printf("d2: %p\n", d2); 
    printf("b1: %p\n", b1); 
    printf("b2: %p\n", b2); 

    j2->say_hi("j2"); 
    j->say_hi(" j"); 
    d1->say_hi("d1"); 
    d2->say_hi("d2"); 
    b1->say_hi("b1"); 
    b2->say_hi("b2"); 

    return 0; 
} 

Il produit la sortie suivante:

j2: 00376C10 
j: 00376C10 
d1: 00376C14 
d2: 00376C18 
b1: 00376C14 
b2: 00376C18 
Hi j2 (00376C10) 
Hi j (00376C10) 
Hi d1 (00376C10) 
Hi d2 (00376C10) 
Hi b1 (00376C10) 
Hi b2 (00376C10) 

Ainsi, lors de la coulée d'un lien2 à ses classes de base, vous pourriez obtenir des pointeurs différents, mais le ce pointeur passé à say_hi () est toujours le même, à peu près comme prévu. Donc, fondamentalement, je ne peux pas reproduire votre problème, ce qui rend difficile de répondre à votre vraie question.

En ce qui concerne wat « virtuel » fait, je trouve l'article sur wikipedia instructif, bien que, aussi, semble se concentrer sur le problème du diamant

0

Comme le dit Caspin, votre premier exemple ne fait pas réellement quelque chose d'utile. Ce qu'il fera cependant, c'est ajouter un vpointer pour indiquer aux classes dérivées où trouver les classes dont il a hérité.

Ceci corrige tous les diamants que vous pouvez maintenant créer (ce que vous n'avez pas), mais comme la structure de classe n'est plus statique, vous ne pouvez plus utiliser static_cast. Je ne suis pas familier avec l'API impliquée, mais ce que Rob Walker dit à propos de IUnkown peut être lié à cela.

En bref, l'héritage normal doit être utilisé lorsque vous avez besoin de vos propres baseclass, cela ne devrait pas être partagé avec les classes « frères et sœurs »: (a est un récipient, b, c, d sont des pièces qui ont chacun un conteneur, e combine ces parties (mauvais exemple, pourquoi ne pas utiliser la composition?))

a a a 
| | | 
b c d <-- b, c and d inherit a normally 
\ |/
    e 

Bien que l'héritage virtuel soit pour quand votre baseclass doit être partagé avec eux. (a est le véhicule, b, c, d sont différentes spécialisations du véhicule, e les combine)

a 
/| \ 
b c d <-- b, c and d inherit a virtually 
\ |/
    d 
Questions connexes