2016-06-19 47 views
5

Ho un risultato che non mi aspettavo dall'ereditarietà multipla, i metodi virtual e i puntatori alle classi di base.Eredità multipla, collisione dei metodi virtuali e puntatori delle classi base


Con d.getStr(), quando d è un'istanza derived, la versione base_2 si chiama, come mi aspettavo.

Con p->getStr(), quando p è un puntatore a un'istanza derived (o un puntatore a base_2 che punta a un'istanza derived), la versione base_2 è chiamato, come mi aspettavo.

Ma con p->getStr(), quando p è un puntatore ad una base_1 che punta a un'istanza derived, la versione base_1 è chiamato ed ero convinto sarebbe chiamato la versione base_2 (grazie alla using e il fatto che getStr() sono virtual metodi) .

Il seguente è un semplice esempio:

#include <iostream> 

struct base_1 
{ 
    virtual std::string getStr() const 
    { return "string from base 1"; } 
}; 

struct base_2 
{ 
    virtual std::string getStr() const 
    { return "string from base 2"; } 
}; 

struct derived : public base_1, public base_2 
{ 
    using base_2::getStr; 
}; 


int main() 
{ 
    derived d; 

    derived * dp = &d; 
    base_1 * bp1 = &d; 
    base_2 * bp2 = &d; 

    std::cout << "from derived:   " << d.getStr() << std::endl; 
    std::cout << "from derived pointer: " << dp->getStr() << std::endl; 
    std::cout << "from base_1 pointer: " << bp1->getStr() << std::endl; 
    std::cout << "from base_2 pointer: " << bp2->getStr() << std::endl; 
} 

L'uscita è il seguente

from derived:   string from base 2 
from derived pointer: string from base 2 
from base_1 pointer: string from base 1 
from base_2 pointer: string from base 2 

so che, di imporre la chiamata di base_2 versione, posso aggiungere nel derived il metodo seguente

std::string getStr() const 
{ return base_2::getStr(); } 

ma le mie domande sono:

1) Perché il puntatore a base_1 (che punta a un'istanza derivata) ignora la direttiva using e chiama la versione base_1 di getStr()?

2) Esiste un modo per imporre la versione base_2 di getStr(), quando derived esempio viene utilizzato da un puntatore base_1, senza ridefinire getStr()?

--- EDIT ---

Grazie per le risposte.

Capisco che stai descrivendo cosa sta succedendo ma il mio dubbio è: la lingua (lo standard) descrive questo aspetto? O è una parte indefinita?

voglio dire: se mi tolgo la direttiva using, ottengo un errore di compilazione (error: request for member getStr is ambiguous), da d.getStr() e da dp->getStr(), perché il compilatore non sa quale versione di getStr() di scegliere.

Ma getStr() sono i metodi virtual. Quindi (ero convinto che) un puntatore di base dovrebbe usare la versione derivata di loro. Ma abbiamo un paio di metodi di collisione.

Dal punto di vista della lingua (standard), uno base_1 (o base_2) è il puntatore autorizzato (o obbligato) a scegliere una delle due versioni dei metodi collidenti che ignorano l'altro?

Forse mi sbaglio ma mi sembra che, in questo modo, i metodi virtual siano gestiti come metodi non virtual.

+1

"using" aiuta solo in visibilità. Non rende "sovraccaricato" la funzione virtuale di base o ne assomiglia. – Arunmu

risposta

0

1) Sembra che il tuo codice stia facendo esattamente quello che dovrebbe fare. Punta a base_1, in modo da ottenere funzioni da base_1 (o una qualsiasi delle sue classi base). Che l'oggetto sia composto sia da base_1 sia base_2 è sconosciuto a quel punto, perché si punta a una classe base, non a una classe derivata.

2) No, è semplicemente impossibile. È necessario infatti sovraccaricare getStr() in derived.

+0

grazie per l'anser ma, riguardo al punto (1), stai descrivendo il comportamento delle classi base/derivate con i metodi normali; 'getStr()' è un metodo 'virtuale' – max66

+0

Questo non importa; il fatto è che le funzioni virtuali possono "saltare" da classi che sono affiancate, solo verticali se si disegna un diagramma di relazione di classe. – JvO

4

vi aspettate che quando si utilizza la parola chiave using nel seguente modo:

struct derived : public base_1, public base_2 
{ 
    using base_2::getStr; 
}; 

Che questo è lo stesso:

struct derived : public base_1, public base_2 
{ 
    void getStr() 
    { 
     base_2::getStr(); 
    } 
}; 

In questo caso, il comportamento che ci si aspetta - - invocando p->getStr(), quando p è un puntatore a un base_1 - finirebbe, infatti, invocando base_2::getStr(). derived override base_1 's getStr(), quindi invocando base_1' s getStr(), attraverso un normale puntatore, si traduce in derived s' getStr() ottenendo invocati, che richiama base_2getStr() metodo.

Tuttavia, questo non è ciò che accade. La parola chiave using non è un alias per inoltrare una chiamata al metodo in questo modo. La parola chiave using non crea un metodo nella classe derived, quindi l'ereditarietà della classe non è interessata e lo non viene sovrascritto nelle sottosezioni. Ed è per questo che invocare il getStr()derived_1 non termina invocando getStr()derived_2.

2

Ciò accade perché la classe derivata deve avere 2 voci vtable per getStr(), una per ogni classe base, in modo che possa risolvere correttamente sia base_1::getStr() e base_2::getStr(). La direttiva using non crea una voce vtable derived::getStr() o sostituisce quella della classe base, ma semplicemente seleziona quale voce della classe base verrà utilizzata. Quando si passa da un puntatore a base_1, il compilatore "vede" solo le voci vtable per le funzioni virtuali da derived e base_1 in modo da risolvere getStr() a base_1::getStr(). La soluzione per aggiungere un esplicito in derived è probabilmente la più pulita, anche se potrebbe essere consigliabile renderla virtuale per far corrispondere le classi di base alla chiarezza.

+0

grazie per la risposta; il mio dubbio è: i 2 vtables nella classe derivata sono imposti dallo standard C++ o sono un dettaglio di implementazione?In altre parole: questo accade perché è imposto dallo standard o altri compilatori sono autorizzati a generare un comportamento diverso? – max66

+0

I meccanismi sono un dettaglio di implementazione, ma poiché le classi base possono essere esse stesse classi derivate con metodi virtuali, i risultati finali devono essere più o meno gli stessi indipendentemente dal meccanismo esatto. L'unica alternativa è che il compilatore possa vedere tutto il codice ed essere in grado di capire qual è il vero tipo di oggetto puntato, il che è altamente non banale. –