2015-06-01 13 views
7

Sto cercando di capire meglio il concetto di ereditarietà virtuale e quali sono i suoi pericoli.ordine di costruttore dell'ereditarietà virtuale

Ho letto in un altro post (Why is Default constructor called in virtual inheritance?) che esso (= ereditarietà virtuale) modifica l'ordine di chiamata del costruttore (la "nonna" viene chiamata per prima, mentre non ha ereditarietà virtuale).

Così ho provato quanto segue per vedere che ho avuto l'idea (VS2013):

#define tracefunc printf(__FUNCTION__); printf("\r\n") 
struct A 
{ 
    A(){ tracefunc; } 

}; 

struct B1 : public A 
{ 
    B1(){ tracefunc; }; 
}; 

struct B2 : virtual public A 
{ 
    B2() { tracefunc; }; 
}; 

struct C1 : public B1 
{ 
    C1() { tracefunc; }; 
}; 

struct C2 : virtual public B2 
{ 
    C2() { tracefunc; }; 
}; 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    A* pa1 = new C1(); 
    A* pa2 = new C2(); 
} 

L'output è:

A::A 
B1::B1 
C1::C1 
A::A 
B2::B2 
C2::C2 

che non è quello che mi aspettavo (mi aspettavo l'ordine di le 2 classi saranno diverse).

Cosa mi manca? Qualcuno può spiegarmi o indirizzarmi a una fonte che lo spieghi meglio?

Grazie!

+1

Cosa ti aspettavi? "nonna" viene sempre chiamata per prima. Non importa che tipo di ereditarietà stai usando. Se era diverso, come potevano le classi di livello inferiore usare i dati dei genitori nei loro costruttori? – ixSci

+0

L'ereditarietà virtuale fa la differenza solo in caso di ereditarietà multipla. Viene fornito per condividere alcune strutture tramite il grafico di ereditarietà. –

risposta

2

Nell'esempio, l'output è previsto. Virtual inheritance entra in gioco nell'istanza quando si ha una classe con ereditarietà multiple le cui classi genitore ereditano anche dalla stessa classe/tipo (ad esempio "il problema dei diamanti"). Nel tuo esempio, le tue classi potrebbero essere configurate per ereditare virtualmente (se necessario altrove nel codice), ma non necessariamente 'virtualmente ereditare' in base al tuo esempio poiché nessuna delle classi derivate (B1/B2/C1/C2) fa più che ereditare direttamente da A.

ad espandersi, ho ottimizzato il vostro esempio per spiegare un po 'di più:

#include <cstdio> 

#define tracefunc printf(__FUNCTION__); printf("\r\n") 
struct A 
{ 
    A() { tracefunc; } 
    virtual void write() { tracefunc; } 
    virtual void read() { tracefunc; } 
}; 

struct B1 : public A 
{ 
    B1() { tracefunc; }; 
    void read(){ tracefunc; } 
}; 

struct C1 : public A 
{ 
    C1() { tracefunc; }; 
    void write(){ tracefunc; } 
}; 

struct B2 : virtual public A 
{ 
    B2() { tracefunc; }; 
    void read(){ tracefunc; } 
}; 

struct C2 : virtual public A 
{ 
    C2() { tracefunc; }; 
    void write(){ tracefunc; } 
}; 

// Z1 inherits from B1 and C1, both of which inherit from A; when a call is made to any 
// of the base function (i.e. A::read or A::write) from the derived class, the call is 
// ambiguous since B1 and C1 both have a 'copy' (i.e. vtable) for the A parent class. 
struct Z1 : public B1, public C1 
{ 
    Z1() { tracefunc; } 
}; 

// Z2 inherits from B2 and C2, both of which inherit from A virtually; note that Z2 doesn't 
// need to inherit virtually from B2 or C2. Since B2 and C2 both virtual inherit from A, when 
// they are constructed, only 1 copy of the base A class is made and the vtable pointer info 
// is "shared" between the 2 base objects (B2 and C2), and the calls are no longer ambiguous 
struct Z2 : public B2, public C2 
{ 
    Z2() { tracefunc; } 
}; 


int _tmain(int argc, _TCHAR* argv[]) 
{ 
    // gets 2 "copies" of the 'A' base since 'B1' and 'C1' don't virtually inherit from 'A' 
    Z1 z1; 
    // gets only 1 "copy" of 'A' base since 'B2' and 'C2' virtualy inherit from 'A' and thus "share" the vtable pointer to the 'A' base 
    Z2 z2; 

    z1.write(); // ambiguous call to write (which one is it .. B1::write() (since B1 inherits from A) or A::write() ?) 
    z1.read(); // ambiguous call to read (which one is it .. C1::read() (since C1 inherits from A) or A::read() ?) 

    z2.write(); // not ambiguous: z2.write() calls C2::write() since it's "virtually mapped" to/from A::write() 
    z2.read(); // not ambiguous: z2.read() calls B2::read() since it's "virtually mapped" to/from A::read() 
    return 0; 
} 

Mentre potrebbe essere "ovvio" per noi esseri umani, che chiamiamo intendiamo fare nel caso della variabile z1, poiché B1 non ha un metodo write, mi aspetterei che il compilatore scelga il metodo C1::write, ma a causa di come funziona la mappatura della memoria degli oggetti, presenta un problema poiché la copia di base di A nell'oggetto C1 potrebbe avere informazioni diverse (puntatori/riferimenti/maniglie) rispetto alla copia della base A nell'oggetto B1 ect (dal momento che ci sono tecnicamente 2 copie della base A); quindi una chiamata a B1::read() { this->write(); } potrebbe dare un comportamento inaspettato (anche se non indefinito).

La parola chiave virtual su un identificatore di classe base rende esplicito che altre classi che ereditano virtualmente dallo stesso tipo di base, otterranno solo 1 copia del tipo di base.

Si noti che il codice sopra riportato non riesce a compilare con errori del compilatore che spiegano le chiamate ambigue per l'oggetto z1. Se commentare la z1.write(); e z1.read(); linee l'uscita (almeno per me) è il seguente:

A::A 
B1::B1 
A::A 
C1::C1 
Z1::Z1 
A::A 
B2::B2 
C2::C2 
Z2::Z2 
C2::write 
B2::read 

Nota le 2 chiamate al A ctor (A::A) prima Z1 è costruito, mentre Z2 ha solo 1 chiamata al costruttore A.

Mi raccomando di leggere the following on virtual inheritance come si approfondisce su alcune delle altre insidie ​​da prendere in considerazione (come il fatto che le classi virtualmente ereditate devono usare l'elenco di inizializzazione per effettuare chiamate di base class ctor, o che si dovrebbe evitare usare cast in stile C quando si esegue un tipo di ereditarietà).

Spiega anche un po 'di più a ciò a cui inizialmente si allude con l'ordinamento costruttore/distruttore e in particolare su come viene eseguito l'ordine quando si utilizza l'ereditarietà virtuale multipla.

Spero che possa aiutare a chiarire un po 'le cose.

0

Non sarà in grado di vedere alcuna differenza nel risultato perché l'uscita sarà lo stesso in una qualsiasi delle seguenti hiearchies classe:

hiearchy 1

class A {}; 

class B2 : virtual public A {}; 

class C2 : virtual public B2 {}; 

hiearchy 2

class A {}; 

class B2 : public A {}; 

class C2 : virtual public B2 {}; 

hiearchy 3

class A {}; 

class B2 : virtual public A {}; 

class C2 : public B2 {}; 

hiearchy 3

class A {}; 

class B2 : public A {}; 

class C2 : public B2 {}; 

In tutti questi casi, A::A() verrà eseguito per primo, seguito da B2::B2(), e quindi C2::C2().

La differenza tra loro è quando viene chiamato A::A(). Viene chiamato da B2::B2() o C2::C2()?

Non sono al 100% chiaro sulla risposta per Hiearchy 1. Penso che B2::B2() debba essere chiamato da C2::C2 poiché B2 è una classe base virtuale di C. A::A() deve essere chiamato da B2:B2() poiché A è una classe base virtuale di B2. Ma potrei sbagliarmi nell'ordine esatto.

In Gerarchia 2, A::A() sarà chiamato da B2::B2(). Dal B2 la classe di base virtual di C2, B2::B2() viene chiamata da C2::C2(). Poiché A è una normale classe base di B2, B2::B2() viene chiamata B2::B2().

In Gerarchia 2, A::A() verrà chiamato da C2::C2(). Poiché A è una classe di base virtuale, A::A() viene chiamato da C2::C2(). B2::B2() viene chiamato dopo che la chiamata a A::A() è stata completata.

In Gerarchia 4, A::A() sarà chiamato da B2::B2(). Penso che questo caso non abbia bisogno di spiegazioni.

Per chiarire il mio dubbio sulla hiearchy 1, ho usato il seguente programma:

#include <iostream> 

class A 
{ 
    public: 
     A(char const *from) { std::cout << "Called from : " << from << std::endl; } 

}; 

class B2 : virtual public A 
{ 
    public: 
     B2() : A("B2::B2()") {} 
}; 

class C2 : virtual public B2 
{ 
    public: 
     C2() : A("C2::C2()") {} 
}; 

int main() 
{ 
    C2 c; 
} 

ho ottenuto il seguente risultato:

Called from : C2::C2() 

Ciò conferma quanto @TC indicato nel suo commento, che è diverso da quello che mi aspettavo. A::A() viene chiamato da C2::C2, non da B2::B2.

+2

Le basi virtuali sono sempre costruite dal costruttore della classe più derivata; il loro ordine di costruzione è una traversata da sinistra a destra in profondità del DAG delle classi base. Pertanto, in 1, 'C2 :: C2()' chiamerà prima 'A :: A()', quindi chiamerà 'B2 :: B2()'. –

+0

@ T.C. grazie per le informazioni. Ho verificato che hai ragione. –

+0

... dove "da sinistra a destra" si riferisce all'ordine di apparizione nel codice e l'attraversamento è _post-order_. – RJFalconer

1

L'output del compilatore è corretto. In realtà, si tratta dell'obiettivo dell'ereditarietà virtuale. L'ereditarietà virtuale ha lo scopo di risolvere il "problema Diamond" nell'ereditarietà multipla. Ad esempio, B eredita da A, C eredita da A e D eredita da B, C. Il diagramma è così:

A 
    | | 
    B C 
    | | 
    D 

Così, D ha due istanze A da B e C. Se A ha funzioni virtuali , c'è un problema.

Ad esempio:

struct A 
{ 
    virtual void foo(){__builtin_printf("A");} 
    virtual void bar(){} 
}; 

struct B : A 
{ 
    virtual void foo(){__builtin_printf("B");} 
}; 

struct C : A 
{ 
    virtual void bar(){} 
}; 

struct D : B, C 
{ 

}; 

int main() 
{ 
    D d; 
    d.foo(); // Error 
} 

Se io uso il mio compilatore XLC per compilare ed eseguire:

messaggio
xlC -+ a.C 

L'errore è così:

a.C:25:7: error: member 'foo' found in multiple base classes of different types 
    d.foo(); // Error 
    ^
a.C:9:18: note: member found by ambiguous name lookup 
    virtual void foo(){__builtin_printf("B");} 
       ^
a.C:3:18: note: member found by ambiguous name lookup 
    virtual void foo(){__builtin_printf("A");} 
       ^
1 error generated. 
Error while processing a.C. 

Il messaggio di errore è molto chiaro, membro 'pippo' trovato in più classi base di diversi tipi. Se aggiungiamo ereditarietà virtuale, il problema è risolto. Perché i diritti di costruzione di A è gestito da D, c'è un'istanza di A.

Torna al codice, lo schema di ereditarietà è simile a questo:

A  A 
|  | 
B1 B2 
|  | 
C1 C2 

non c'e 'problema diamond', questo è solo un'eredità. Quindi, l'ordine di costruzione è anche A-> B2-> C2, non c'è differenza di output.

Problemi correlati