Qual è il problema esatto con l'ereditarietà multipla?


121

Vedo persone che chiedono continuamente se l'ereditarietà multipla debba essere inclusa nella prossima versione di C # o Java. La gente del C ++, che ha la fortuna di avere questa capacità, dice che è come dare a qualcuno una corda per impiccarsi.

Qual è il problema con l'ereditarietà multipla? Ci sono campioni di cemento?


54
Vorrei solo menzionare che il C ++ è ottimo per darti abbastanza corda per impiccarti.
tloach

1
Per un'alternativa all'ereditarietà multipla che affronti (e, IMHO risolve) molti degli stessi problemi, guarda Traits ( iam.unibe.ch/~scg/Research/Traits )
Bevan

52
Pensavo che il C ++ ti desse abbastanza corda per spararti ai piedi.
KeithB

6
Questa domanda sembra presumere che ci sia un problema con MI in generale, mentre ho trovato molte lingue in cui MI è in uso occasionale. Ci sono certamente problemi con la gestione di MI in alcune lingue, ma non sono a conoscenza del fatto che MI in generale abbia problemi significativi.
David Thornley,

Risposte:


86

Il problema più ovvio è con l'override della funzione.

Supponiamo di avere due classi Ae B, entrambe definiscono un metodo doSomething. Ora definisci una terza classe C, che eredita da Ae B, ma non sovrascrivi il doSomethingmetodo.

Quando il compilatore esegue il seed di questo codice ...

C c = new C();
c.doSomething();

... quale implementazione del metodo dovrebbe utilizzare? Senza ulteriori chiarimenti, è impossibile per il compilatore risolvere l'ambiguità.

Oltre all'override, l'altro grosso problema con l'ereditarietà multipla è il layout degli oggetti fisici in memoria.

Linguaggi come C ++ e Java e C # creano un layout basato su indirizzi fissi per ogni tipo di oggetto. Qualcosa come questo:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

Quando il compilatore genera codice macchina (o bytecode), utilizza quegli offset numerici per accedere a ciascun metodo o campo.

L'ereditarietà multipla lo rende molto complicato.

Se la classe Ceredita da Ae B, il compilatore deve decidere se disporre i dati in ABordine o in BAordine.

Ma ora immagina di chiamare metodi su un Boggetto. È davvero solo un B? O è effettivamente un Coggetto chiamato polimorficamente, attraverso la sua Binterfaccia? A seconda dell'identità effettiva dell'oggetto, il layout fisico sarà diverso ed è impossibile conoscere l'offset della funzione da richiamare nel sito di chiamata.

Il modo per gestire questo tipo di sistema è abbandonare l'approccio del layout fisso, consentendo a ogni oggetto di essere interrogato per il suo layout prima di tentare di invocare le funzioni o accedere ai suoi campi.

Quindi ... per farla breve ... è una seccatura per gli autori di compilatori supportare l'ereditarietà multipla. Quindi, quando qualcuno come Guido van Rossum progetta python, o quando Anders Hejlsberg progetta c #, sa che supportare l'ereditarietà multipla renderà le implementazioni del compilatore significativamente più complesse e presumibilmente non pensano che il vantaggio valga il costo.


62
Ehm, Python supporta MI
Nemanja Trifunovic

26
Questi non sono argomenti molto convincenti - la cosa del layout fisso non è affatto complicata nella maggior parte delle lingue; in C ++ è complicato perché la memoria non è opaca e quindi potresti incontrare qualche difficoltà con i presupposti aritmetici dei puntatori. Nei linguaggi in cui le definizioni di classe sono statiche (come in java, C # e C ++), più conflitti di nomi di ereditarietà possono essere vietati in fase di compilazione (e C # lo fa comunque con le interfacce!).
Eamon Nerbonne

10
L'OP voleva solo capire i problemi e li ho spiegati senza redigere personalmente la questione. Ho appena detto che i progettisti del linguaggio e gli implementatori del compilatore "presumibilmente non pensano che il vantaggio valga il costo".
Benjismith

12
" Il problema più ovvio è con l'override della funzione. " Questo non ha nulla a che fare con l'override della funzione. È un semplice problema di ambiguità.
curioso

10
Questa risposta ha alcune informazioni sbagliate su Guido e Python, poiché Python supporta MI. "Ho deciso che fintanto che avrei sostenuto l'ereditarietà, potevo anche supportare una versione ingenua dell'ereditarietà multipla". - Guido van Rossum python-history.blogspot.com/2009/02/… - Inoltre, la risoluzione dell'ambiguità è abbastanza comune nei compilatori (le variabili possono essere da locale a blocco, da locale a funzione, da locale a inclusiva, membri oggetto, membri di classe, globali, ecc.), non vedo come uno scope aggiuntivo potrebbe fare la differenza.
marcus

46

I problemi che menzionate non sono poi così difficili da risolvere. In effetti, ad esempio, Eiffel lo fa perfettamente! (e senza introdurre scelte arbitrarie o altro)

Ad esempio, se erediti da A e B, entrambi con metodo foo (), allora ovviamente non vuoi che una scelta arbitraria nella tua classe C erediti sia da A che da B. Devi ridefinire foo in modo che sia chiaro cosa sarà usato se si chiama c.foo () o altrimenti bisogna rinominare uno dei metodi in C. (potrebbe diventare bar ())

Inoltre penso che l'ereditarietà multipla sia spesso molto utile. Se guardi le librerie di Eiffel vedrai che è usato ovunque e personalmente ho perso la funzione quando sono dovuto tornare alla programmazione in Java.


26
Sono d'accordo. Il motivo principale per cui le persone odiano MI è lo stesso di JavaScript o della digitazione statica: la maggior parte delle persone ne ha usato solo pessime implementazioni o l'ha usata molto male. Giudicare MI da C ++ è come giudicare OOP da PHP o giudicare automobili da Pintos.
Jörg W Mittag

2
@curiousguy: MI introduce ancora un'altra serie di complicazioni di cui preoccuparsi, proprio come molte delle "funzionalità" di C ++. Solo perché non è ambiguo non è facile lavorare o eseguire il debug. Rimuovendo questa catena da quando è andata fuori tema e l'hai persa comunque.
Guvante

4
@Guvante l'unico problema con MI in qualsiasi lingua è che i programmatori di merda pensano di poter leggere un tutorial e improvvisamente conoscono una lingua.
Miles Rout

2
Direi che le funzionalità del linguaggio non riguardano solo la riduzione del tempo di codifica. Si tratta anche di aumentare l'espressività di una lingua e aumentare le prestazioni.
Miglia rotta

4
Inoltre, i bug si verificano solo da MI quando gli idioti lo usano in modo errato.
Miglia rotta

27

Il problema del diamante :

un'ambiguità che sorge quando due classi B e C ereditano da A e la classe D eredita sia da B che da C.Se c'è un metodo in A che B e C hanno sovrascritto e D non lo sovrascrive, allora quale versione del metodo eredita D: quello di B o quello di C?

... Si chiama "problema del diamante" a causa della forma del diagramma di ereditarietà delle classi in questa situazione. In questo caso, la classe A è in alto, sia B che C separatamente sotto di essa, e D unisce i due insieme in basso per formare una forma di diamante ...


4
che ha una soluzione nota come eredità virtuale. È solo un problema se lo fai male.
Ian Goldby

1
@IanGoldby: l'ereditarietà virtuale è un meccanismo per risolvere parte del problema, se non è necessario consentire upcast e downcast che preservano l'identità tra tutti i tipi da cui un'istanza è derivata o per cui è sostituibile . Dato X: B; Y: B; e Z: X, Y; supponiamo che someZ sia un'istanza di Z. Con l'ereditarietà virtuale, (B) (X) someZ e (B) (Y) someZ sono oggetti distinti; dato uno dei due, uno potrebbe ottenere l'altro tramite un abbattuto e un upcast, ma cosa succede se uno ha un someZe vuole lanciarlo Objecte poi farlo B? Quale Botterrà?
supercat

2
@supercat Forse, ma problemi del genere sono in gran parte teorici e in ogni caso possono essere segnalati dal compilatore. L'importante è essere consapevoli di quale problema si sta cercando di risolvere, e quindi utilizzare lo strumento migliore, ignorando il dogma delle persone che preferirebbero non preoccuparsi di capire "perché?"
Ian Goldby

@IanGoldby: problemi del genere possono essere segnalati dal compilatore solo se ha accesso simultaneo a tutte le classi in questione. In alcuni framework, qualsiasi modifica a una classe base richiederà sempre una ricompilazione di tutte le classi derivate, ma la possibilità di utilizzare versioni più recenti delle classi base senza dover ricompilare le classi derivate (per le quali si potrebbe non avere il codice sorgente) è una caratteristica utile per i framework che possono fornirlo. Inoltre, i problemi non sono solo teorici. Molte classi in .NET si basano sul fatto che un cast da qualsiasi tipo di riferimento Objecte ritorno a quel tipo ...
supercat

3
@ IanGoldby: abbastanza giusto. Il punto era che gli implementatori di Java e .NET non erano solo "pigri" nel decidere di non supportare l'MI generalizzata; sostenere l'IM generalizzata avrebbe impedito alla loro struttura di sostenere vari assiomi la cui validità è più utile per molti utenti di quanto lo sarebbe l'MI.
supercat

21

L'ereditarietà multipla è una di quelle cose che non viene utilizzata spesso e può essere utilizzata in modo improprio, ma a volte è necessaria.

Non ho mai capito di non aggiungere una funzionalità, solo perché potrebbe essere utilizzata in modo improprio, quando non ci sono buone alternative. Le interfacce non sono un'alternativa all'ereditarietà multipla. Per prima cosa, non ti consentono di applicare precondizioni o postcondizioni. Proprio come qualsiasi altro strumento, devi sapere quando è appropriato usarlo e come usarlo.


Puoi spiegare perché non ti consentono di applicare le condizioni pre e post?
Yttrill

2
@Yttrill perché le interfacce non possono avere implementazioni del metodo. Dove metti il assert?
Curioso

1
@curiousguy: usi un linguaggio con una sintassi adeguata che ti permette di inserire le condizioni pre e post direttamente nell'interfaccia: non è necessario "assert". Esempio da Felix: fun div (num: int, den: int quando den! = 0): int si aspetta risultato == 0 implica num == 0;
Yttrill

@Yttrill OK, ma alcuni linguaggi, come Java, non supportano MI o "pre e post condizioni direttamente nell'interfaccia".
curioso

Non è usato spesso perché non è disponibile e non sappiamo come usarlo bene. Se dai un'occhiata ad un po 'di codice Scala, vedrai come le cose iniziano ad essere comuni e possono essere refactoring in tratti (Ok, non è MI, ma dimostra il mio punto).
santiagobasulto

16

diciamo che hai oggetti A e B che sono entrambi ereditati da C. A e B entrambi implementano foo () e C no. Chiamo C.foo (). Quale implementazione viene scelta? Ci sono altri problemi, ma questo tipo di cose è importante.


1
Ma non è proprio un esempio concreto. Se sia A che B hanno una funzione, è molto probabile che anche C necessiti della propria implementazione. Altrimenti può ancora chiamare A :: foo () nella sua funzione foo ().
Peter Kühne

@ Quantum: E se non fosse così? È facile vedere il problema con un livello di ereditarietà, ma se hai molti livelli e hai una funzione casuale che è da qualche parte il doppio, questo diventa un problema molto difficile.
tloach

Inoltre, il punto non è che non puoi chiamare il metodo A o B specificando quello che vuoi, il punto è che se non specifichi non c'è un buon modo per sceglierne uno. Non sono sicuro di come il C ++ gestisca questo, ma se qualcuno lo sa potrebbe menzionarlo?
tloach

2
@tloach - se C non risolve l'ambiguità, il compilatore può rilevare questo errore e restituire un errore in fase di compilazione.
Eamon Nerbonne

@Earmon - A causa del polimorfismo, se foo () è virtuale, il compilatore potrebbe non sapere nemmeno in fase di compilazione che questo sarà un problema.
tloach

5

Il problema principale con l'ereditarietà multipla è ben riassunto con l'esempio di tloach. Quando si eredita da più classi base che implementano la stessa funzione o campo, è il compilatore a dover prendere una decisione su quale implementazione ereditare.

La situazione peggiora quando erediti da più classi che ereditano dalla stessa classe di base. (eredità del diamante, se disegni l'albero dell'eredità ottieni una forma a diamante)

Questi problemi non sono realmente problematici da superare per un compilatore. Ma le scelte che il compilatore deve fare qui sono piuttosto arbitrarie, questo rende il codice molto meno intuitivo.

Trovo che quando eseguo un buon design OO non ho mai bisogno di eredità multipla. Nei casi in cui ne ho bisogno, di solito trovo che sto usando l'ereditarietà per riutilizzare la funzionalità mentre l'ereditarietà è appropriata solo per le relazioni "è-a".

Esistono altre tecniche come i mixin che risolvono gli stessi problemi e non hanno i problemi che ha l'ereditarietà multipla.


4
Il compilato non ha bisogno di fare una scelta arbitraria - può semplicemente sbagliare. In C #, qual è il tipo di ([..bool..]? "test": 1)?
Eamon Nerbonne

4
In C ++, il compilatore non fa mai scelte così arbitrarie: è un errore definire una classe in cui il compilatore avrebbe bisogno di fare una scelta arbitraria.
curioso

5

Non credo che il problema del diamante sia un problema, considererei quel sofisma, nient'altro.

Il problema peggiore, dal mio punto di vista, con l'ereditarietà multipla è RAD - vittime e persone che affermano di essere sviluppatori ma in realtà sono bloccate con una mezza conoscenza (nella migliore delle ipotesi).

Personalmente, sarei molto felice se potessi finalmente fare qualcosa in Windows Forms come questo (non è il codice corretto, ma dovrebbe darti l'idea):

public sealed class CustomerEditView : Form, MVCView<Customer>

Questo è il problema principale che ho con l'assenza di eredità multipla. PUOI fare qualcosa di simile con le interfacce, ma c'è quello che io chiamo "s *** code", è questo doloroso ripetitivo c *** che devi scrivere in ciascuna delle tue classi per ottenere un contesto dati, per esempio.

A mio parere, non dovrebbe esserci assolutamente alcuna necessità, nemmeno la minima, di NESSUNA ripetizione di codice in un linguaggio moderno.


Tendo ad essere d'accordo, ma solo tendenzialmente: c'è bisogno di una certa ridondanza in qualsiasi lingua per rilevare gli errori. In ogni caso dovresti unirti al team di sviluppatori Felix perché questo è un obiettivo fondamentale. Ad esempio, tutte le dichiarazioni sono reciprocamente ricorsive e puoi vedere sia in avanti che all'indietro, quindi non hai bisogno di dichiarazioni anticipate (l'ambito è impostato, come le etichette C goto).
Yttrill

Sono completamente d'accordo con questo - ho appena incontrato un problema simile qui . La gente parla del problema dei diamanti, lo cita religiosamente, ma secondo me è così facilmente evitabile. (Non tutti abbiamo bisogno di scrivere i nostri programmi come hanno scritto la libreria iostream.) L'ereditarietà multipla dovrebbe essere logicamente usata quando si ha un oggetto che necessita della funzionalità di due classi base differenti che non hanno funzioni o nomi di funzioni sovrapposti. Nelle mani giuste, è uno strumento.
jedd.ahyoung

3
@Turing Complete: non avendo alcuna ripetizione del codice: questa è una bella idea ma non è corretta e impossibile. Ci sono un numero enorme di modelli di utilizzo e desideriamo astrarre quelli comuni nella libreria, ma è follia astrarli tutti perché anche se potessimo il carico semantico di ricordare tutti i nomi è troppo alto. Quello che vuoi è un buon equilibrio. Non dimenticare che la ripetizione è ciò che dà struttura alle cose (il modello implica ridondanza).
Yttrill

@ lunchmeat317: Il fatto che il codice in genere non dovrebbe essere scritto in modo tale che il "diamante" costituisca un problema, non significa che un progettista di linguaggio / framework possa semplicemente ignorare il problema. Se un framework prevede che l'upcasting e il downcasting preservino l'identità dell'oggetto, desidera consentire alle versioni successive di una classe di aumentare il numero di tipi per i quali può essere sostituita senza che ciò sia una modifica sostanziale e desidera consentire la creazione di tipi in fase di esecuzione, Non penso che possa consentire l'ereditarietà di più classi (al contrario dell'ereditarietà dell'interfaccia) mentre si soddisfano gli obiettivi di cui sopra.
supercat

3

Il Common Lisp Object System (CLOS) è un altro esempio di qualcosa che supporta MI evitando i problemi in stile C ++: all'ereditarietà viene dato un valore predefinito ragionevole , pur consentendo la libertà di decidere esplicitamente come esattamente, ad esempio, chiamare il comportamento di un super .


Sì, CLOS è uno dei sistemi a oggetti più avanzati dall'inizio dell'informatica moderna forse anche molto tempo fa :)
rostamn739

2

Non c'è niente di sbagliato nell'ereditarietà multipla stessa. Il problema è aggiungere l'ereditarietà multipla a una lingua che non è stata progettata pensando all'ereditarietà multipla sin dall'inizio.

Il linguaggio Eiffel supporta l'ereditarietà multipla senza restrizioni in un modo molto efficiente e produttivo, ma il linguaggio è stato progettato sin dall'inizio per supportarlo.

Questa funzionalità è complessa da implementare per gli sviluppatori di compilatori, ma sembra che tale inconveniente possa essere compensato dal fatto che un buon supporto per l'ereditarietà multipla potrebbe evitare il supporto di altre funzionalità (cioè nessuna necessità di interfaccia o metodo di estensione).

Penso che supportare l'ereditarietà multipla o meno sia più una questione di scelta, una questione di priorità. Una funzionalità più complessa richiede più tempo per essere implementata correttamente e operativa e potrebbe essere più controversa. L'implementazione C ++ potrebbe essere il motivo per cui l'ereditarietà multipla non è stata implementata in C # e Java ...


1
Il supporto C ++ per MI non è " molto efficiente e produttivo "?
Curioso

1
In realtà è un po 'rotto nel senso che non si adatta ad altre funzionalità di C ++. L'assegnazione non funziona correttamente con l'ereditarietà, per non parlare dell'ereditarietà multipla (controlla le regole davvero pessime). Creare diamanti correttamente è così difficile che il comitato per gli standard ha incasinato la gerarchia delle eccezioni per mantenerla semplice ed efficiente, piuttosto che farlo correttamente. Su un vecchio compilatore che stavo usando al momento ho provato questo e alcuni mixin MI e implementazioni di eccezioni di base costano più di un megabyte di codice e ci sono voluti 10 minuti per compilare .. solo le definizioni.
Yttrill

1
I diamanti sono un buon esempio. In Eiffel, il diamante è risolto esplicitamente. Ad esempio, immagina che Studente e Insegnante ereditino entrambi da Persona. La persona ha un calendario, quindi sia lo studente che l'insegnante erediteranno questo calendario. Se costruisci un diamante creando un TeachingStudent che eredita sia dall'insegnante che dallo studente, puoi decidere di rinominare uno dei calendari ereditati per mantenere entrambi i calendari disponibili separatamente o decidere di unirli in modo che si comporti più come Persona. L'ereditarietà multipla può essere implementata bene, ma richiede una progettazione attenta preferibilmente dall'inizio ...
Christian Lemer

1
I compilatori Eiffel devono eseguire un'analisi del programma globale per implementare questo modello di MI in modo efficiente. Per le chiamate ai metodi polimorfici usano thunk del dispatcher o matrici sparse come spiegato qui . Questo non si combina bene con la compilazione separata di C ++ e con la funzionalità di caricamento delle classi di C # e Java.
cyco130

2

Uno degli obiettivi di progettazione di framework come Java e .NET è di consentire al codice compilato di funzionare con una versione di una libreria precompilata, di funzionare altrettanto bene con le versioni successive di quella libreria, anche se quelle versioni successive aggiungere nuove funzionalità. Mentre il paradigma normale in linguaggi come C o C ++ è quello di distribuire eseguibili collegati staticamente che contengono tutte le librerie di cui hanno bisogno, il paradigma in .NET e Java è quello di distribuire le applicazioni come raccolte di componenti che sono "collegati" in fase di esecuzione .

Il modello COM che ha preceduto .NET ha tentato di utilizzare questo approccio generale, ma in realtà non aveva ereditarietà - invece, ogni definizione di classe definisce efficacemente sia una classe che un'interfaccia con lo stesso nome che conteneva tutti i suoi membri pubblici. Le istanze erano del tipo di classe, mentre i riferimenti erano del tipo di interfaccia. Dichiarare una classe come derivante da un'altra equivaleva a dichiarare una classe come implementazione dell'interfaccia dell'altra e richiedeva che la nuova classe reimplementasse tutti i membri pubblici delle classi da cui una derivava. Se Y e Z derivano da X, e quindi W deriva da Y e Z, non importa se Y e Z implementano i membri di X in modo diverso, perché Z non sarà in grado di usare le loro implementazioni - dovrà definirne proprio. W potrebbe incapsulare istanze di Y e / o Z,

La difficoltà in Java e .NET è che il codice può ereditare i membri e gli accessi ad essi si riferiscono implicitamente ai membri principali. Supponiamo che uno avesse classi WZ correlate come sopra:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

Sembrerebbe che la W.Test()creazione di un'istanza di W richiami l'implementazione del metodo virtuale Foodefinito in X. Supponiamo, tuttavia, che Y e Z fossero effettivamente in un modulo compilato separatamente, e sebbene fossero definiti come sopra quando X e W sono stati compilati, sono stati successivamente modificati e ricompilati:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

Ora quale dovrebbe essere l'effetto della chiamata W.Test()? Se il programma dovesse essere collegato staticamente prima della distribuzione, la fase di collegamento statico potrebbe essere in grado di discernere che mentre il programma non aveva ambiguità prima che Y e Z venissero modificati, le modifiche a Y e Z hanno reso le cose ambigue e il linker potrebbe rifiutarsi di farlo costruire il programma a meno che o fino a quando tale ambiguità non viene risolta. D'altra parte, è possibile che la persona che ha sia W che le nuove versioni di Y e Z sia qualcuno che vuole semplicemente eseguire il programma e non ha codice sorgente per nessuno di essi. Quando W.Test()viene eseguito, non sarebbe più chiaro cosaW.Test() dovrebbe funzionare, ma fino a quando l'utente non avesse provato a eseguire W con la nuova versione di Y e Z non ci sarebbe stato alcun modo in cui nessuna parte del sistema potesse riconoscere che c'era un problema (a meno che W non fosse considerato illegittimo anche prima delle modifiche a Y e Z) .


2

Il diamante non è un problema, a patto che non usi nulla come l'ereditarietà virtuale C ++: nell'ereditarietà normale ogni classe base assomiglia a un campo membro (in realtà sono disposte nella RAM in questo modo), dandoti un po 'di zucchero sintattico e un capacità aggiuntiva di sovrascrivere metodi più virtuali. Ciò può imporre alcune ambiguità in fase di compilazione, ma di solito è facile da risolvere.

D'altra parte, con l'eredità virtuale va troppo facilmente fuori controllo (e poi diventa un pasticcio). Considera come esempio un diagramma a "cuore":

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

In C ++ è del tutto impossibile: non appena Fe Gvengono fusi in una singola classe, anche le loro Avengono unite, punto. Ciò significa che potresti non considerare mai le classi base opache in C ++ (in questo esempio devi costruire Ain Hmodo da sapere che è presente da qualche parte nella gerarchia). In altre lingue potrebbe funzionare, tuttavia; per esempio, Fe Gpotrebbe dichiarare esplicitamente A come "interna", impedendo così la conseguente fusione e rendendosi effettivamente solidi.

Un altro esempio interessante ( non specifico per C ++):

  A
 / \
B   B
|   |
C   D
 \ /
  E

Qui Butilizza solo l'ereditarietà virtuale. Quindi Econtiene due messaggi Bche condividono lo stesso A. In questo modo, puoi ottenere un A*puntatore che punta a E, ma non puoi lanciarlo a un B*puntatore sebbene l'oggetto sia effettivamente B come tale il cast è ambiguo e questa ambiguità non può essere rilevata in fase di compilazione (a meno che il compilatore non veda il intero programma). Ecco il codice di prova:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

Inoltre, l'implementazione può essere molto complessa (dipende dalla lingua; vedere la risposta di benjismith).


Questo è il vero problema con MI. I programmatori potrebbero aver bisogno di diverse risoluzioni all'interno di una classe. Una soluzione a livello di linguaggio limiterebbe ciò che è possibile e costringerebbe i programmatori a creare kludge per far funzionare correttamente il programma.
shawnhcorey
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.