Qual è la differenza tra la progettazione interna di Java e C ++ che consente a C ++ di avere ereditarietà multipla? [duplicare]

25

È eseguito il drill sui programmatori Java newbie che Java ( pre-Java 8 ) non ha ereditarietà di più classi e solo più ereditarietà dell'interfaccia, perché altrimenti si verifica un problema di ereditarietà del diamante ( Classe A eredita dalle classi B e C, entrambe implementano il metodo X. Quindi quale implementazione di quelle classi viene utilizzata quando si esegue una chiamata aX ()? )

Chiaramente, C ++ ha affrontato con successo questo, dal momento che ha ereditarietà multipla.

Cosa c'è di diverso tra la progettazione interna di Java e C ++ - (indovino nella metodologia di invio del metodo e ricordo vagamente che aveva a che fare con i vtables della mia classe di lingue informatiche di lunga data) che consente a questo problema di essere risolto in C ++ ma impedisce in Java?

Per riformulare:

Qual è la metodologia della progettazione / implementazione del linguaggio utilizzata in C ++ per evitare l'ambiguità del problema dell'ereditarietà del diamante e perché non è possibile utilizzare la stessa metodologia in Java ?

    
posta DVK 30.12.2014 - 19:54
fonte

9 risposte

69

Non c'è nulla di fondamentale nel design interno che causi questo. La mancanza di ereditarietà multipla è una decisione progettuale intenzionale in Java, non una manifestazione esterna di una mancanza nella progettazione interna.

(sto deliberatamente evitando di entrare nella guerra di fiamma se l'eredità multipla è una buona idea o no).

    
risposta data 30.12.2014 - 20:00
fonte
42

Clearly, C++ successfully addressed this, since it has multiple inheritance.

In realtà no, esattamente l'opposto. C ++ non ha risolto questo problema, e il problema dell'eredità del diamante è un problema serio per gli sviluppatori C ++ . Esistono tecniche per affrontarlo usando "l'ereditarietà virtuale", ma spesso rendono le cose più complicate.

Java e altri linguaggi successivi al C ++ non hanno fallito nell'implementare l'ereditarietà multipla perché non potevano risolverlo nello stesso modo in cui lo faceva C ++; hanno imparato dagli errori di C ++ e non hanno implementato una funzione che introduce problemi che non sono facilmente risolvibili.

    
risposta data 30.12.2014 - 20:37
fonte
27

La differenza fondamentale è che Java definisce le conversioni da un tipo a qualsiasi del suo supertipo come preservazione dell'identità. Questa è una proprietà utile da avere, ma è incompatibile con l'ereditarietà multipla. Se d1 e d2 derivano entrambe da b e thing è di un tipo che deriva sia da d1 sia da d2 , quindi in assenza di severe restrizioni su cosa d1 e d2 può fare, è possibile che (b)(d1)thing e (b)(d2)thing si comportino diversamente. Se tutti i cast da thing a b sono richiesti per passare attraverso d1 o d2 , allora è possibile che (b)(d1)thing e (b)(d2)thing producano oggetti diversi, ma ciò implicherebbe che i cast non siano identità -preserving.

Un'altra difficoltà che si presenta è che C ++ è progettato per essere collegato staticamente, il che significa che qualsiasi cosa nella progettazione di tipi d1 e d2 che renderebbe ambiguo l'uso di membri comuni verrebbe catturata al momento del collegamento. Se i fornitori di d1 e d2 aggiungono simultaneamente nuove funzionalità che sono incompatibili tra loro, anche se ognuna sarebbe compatibile con la versione precedente dell'altra classe, la persona che fornisce il programma che utilizza entrambe dovrebbe trovare una combinazione di classi che funzionerebbero prima che il programma potesse essere collegato, ma i consumatori del programma sarebbero quindi certi che le versioni in bundle di d1 e d2 sarebbero compatibili l'una con l'altra.

In Java, è previsto che parti diverse di un programma possano essere aggiornate separatamente. Se l'autore di una classe rilascia una nuova versione, il codice che usa quella classe può essere usato per usare la nuova versione senza dover coinvolgere il programmatore originale del codice che consuma. Una conseguenza di ciò è che se certe combinazioni di versioni di classi diverse funzioneranno, ma alcune combinazioni genereranno fastidiose ambiguità, è probabile che alcuni di questi problemi saranno scoperti solo da alcuni sfortunati utenti finali che hanno le stesse combinazioni problematiche di versioni.

Sebbene possano verificarsi problemi di versioning in ogni caso, l'ereditarietà multipla aggiunge alcuni tipi di insuccessi che sarebbe molto difficile da prevedere. Se il codice è autorizzato a utilizzare un membro che esiste in una superclasse ma non nell'altra senza dover specificare la superclasse, l'aggiunta di un membro con nome simile all'altra superclasse diventerebbe una modifica irrisolta. Poiché l'autore di una classe generalmente non ha idea di quali altre classi possa essere combinato, un tale autore non avrebbe modo di sapere quali nomi sono "sicuri" da usare.

Per essere sicuri, alcuni dei suddetti problemi possono sorgere con la nuova funzionalità di "implementazione dell'interfaccia predefinita" di Java; il valore della funzione è stato giudicato sufficiente per giustificare l'accettazione di tali elementi. Tuttavia, vale la pena notare che il collegamento dinamico delle classi Java rende i compromessi molto diversi da quelli che sarebbero in un linguaggio collegato staticamente come il C ++.

    
risposta data 30.12.2014 - 21:43
fonte
17

Il problema dell'ereditarietà del diamante non è risolto in C ++, nel senso che il C ++ ti permette di spararti ai piedi in questo modo.

Il problema è risolto in Java perché è stata presa una decisione progettuale intenzionale per impedire l'ereditarietà multipla delle implementazioni, omettendo così l'elemento di progettazione dal quale si pone il problema.

Il vero problema con l'ereditarietà multipla è che la sua complessità supera il suo potenziale beneficio. Questo è principalmente il motivo per cui è stato omesso da Java, non a causa del problema dell'eredità del diamante.

    
risposta data 30.12.2014 - 20:00
fonte
16

Per rispondere alla domanda originale dell'OP, esiste una funzionalità di implementazione significativa in C ++ non presente in Java che consente l'ereditarietà multipla. La differenza è che C ++ deve utilizzare trucchi speciali per correggere il puntatore "this" prima di immettere una funzione virtuale in una classe con più classi base concrete. Questi trucchi sono vtable thunk e double-wide vtables.

Gli oggetti C ++ sono disposti in memoria come un blocco contiguo. Ogni classe derivata aggiunge i dati al blocco. Le classi con una catena ereditaria singola partono da un punto in memoria e tutte le classi base e gli antenati della classe più derivata hanno lo stesso puntatore "questo". Le interfacce non hanno alcun dato quindi non influenzano il layout dell'oggetto. Il puntatore "this" di tutte le basi rimane lo stesso. Questo è vero in C ++, Java, C # e in altri linguaggi che supportano oggetti con una singola catena di ereditarietà di classe concreta insieme a qualsiasi numero di interfacce.

Per una classe con più basi concrete, solo il layout della prima catena di ereditarietà può essere contiguo e coincidente con la classe più derivata. Il layout effettivo dipende dall'implementazione e può diventare piuttosto complesso, specialmente quando ci sono diverse catene e diverse copie ripetute di alcuni oggetti di base. In ogni caso, il puntatore "this" cambia a seconda del punto di vista di ogni funzione membro. Questo non è un problema per le funzioni statiche o non virtuali, poiché il puntatore "this" viene calcolato al momento della compilazione. Tuttavia, C ++ deve utilizzare trucchi speciali per correggere il puntatore "this" prima di entrare in una funzione virtuale.

C'erano due strategie popolari per questo nei primi giorni del C ++. Uno era un thunk vtable e l'altro era double-wide vtables. Puoi leggere maggiori dettagli su di loro in Inside the C ++ Object Model di Stanley B Lippman. Essenzialmente: 1) il thunk del vtable sostituisce l'indirizzo della funzione reale nel vtable con l'indirizzo di un thunk runtime (un pezzo anonimo di codice) che regola il puntatore "this", quindi i JMP nella funzione membro. Il costo è un leggero overhead aggiuntivo per chiamare una funzione virtuale in una classe con più classi base. 2) i vtables double-wide memorizzano ancora l'indirizzo della funzione membro nel vtable ma anche il relativo puntatore "this" per quella funzione (o un modo per calcolarlo). Il costo è che il vtable occupa il doppio dello spazio per le classi con più basi.

NOTA: Le classi di base virtuali, incluse più classi di base virtuali, sono un concetto completamente diverso che utilizza un'implementazione completamente diversa. La strategia comune per l'implementazione di VBC sta estendendo il vtable all'indietro in offset negativi con i puntatori ai blocchi di dati (non necessariamente contigui) delle classi virtuali.

Naturalmente, una classe che ha sia classi base virtuali che più classi base concrete sarà davvero un mostro peloso. Mi meraviglio sempre che C ++ compili anche.

    
risposta data 30.12.2014 - 23:21
fonte
11

Entrambi i sistemi sottostanti Java e C ++ sono abbastanza potenti da consentire / supportare l'ereditarietà multipla.

Nessuno dei due sistemi fornisce soluzioni integrate (o altra assistenza) per aiutare gli sviluppatori a gestire le conseguenze / i potenziali problemi.

In C ++, è stata presa la decisione cosciente di esporre quanto più possibile la potenza del sistema sottostante agli sviluppatori e consentire loro di decidere quali sottoinsiemi del sistema vale la pena utilizzare, e quali no. Una buona parte della decisione, inizialmente, probabilmente aveva a che fare con le implementazioni del compilatore iniziale che non dovevano fare molto lavoro extra per supportare l'ereditarietà multipla. La decisione è rimasta bloccata.

Per quanto riguarda Java, vedi qui per un inizio (1995) spiegazione. La decisione esplicita è stata presa per impedire che l'ereditarietà multipla fosse possibile, a livello linguistico: si riteneva che fosse troppo "un'arma da fuoco".

    
risposta data 30.12.2014 - 21:10
fonte
1

Potrei leggere male la tua domanda, ma ci provo perché sospetto che sia una domanda che ho avuto anche a un certo punto.

Potrebbe sembrare che Java non abbia un'ereditarietà di implementazione multipla perché è difficile da implementare, mentre C ++ lo ha perché i suoi implementatori hanno trovato un modo per implementarlo. Questo non è il caso. Come altri hanno già detto, è stata deliberata una decisione progettuale di evitare una categoria di problemi di programmazione, non qualcosa che permettesse una più facile implementazione di Java.

Il fatto stesso che Java abbia ereditarietà multipla dalle interfacce significa che i suoi implementatori hanno fatto il duro lavoro di implementare l'ereditarietà multipla in ogni caso - il fatto che le interfacce siano astratte è per lo più irrilevante.

In un'implementazione che utilizza vtables, per implementare il polimorfismo sotto ereditarietà singola, è necessario un vtable del tipo runtime dell'istanza, perferenzialmente in una posizione predeterminata. Per implementare il polimorfismo sotto ereditarietà multipla, sono necessari più vtables.

Per semplicità, diciamo che il layout di memoria di ogni tipo inizia con il suo vtable (non è richiesto che sia così, ma renderà la mia descrizione più semplice).

Type A [vtableA, mem1, mem2, ...]
Type B [vtableB, mem3, mem4, ...]
Type C : A, B [vtableA, mem1, mem2, ..., vtableB, mem3, mem4,...]
C* from C     ^

Nell'ambito dell'ereditarietà singola, devi solo passare il riferimento all'oggetto e puoi sempre trovare il vtable del suo tipo di runtime.

Type C : A, B [vtableA, mem1, mem2, ..., vtableB, mem3, mem4,...]
A* from C     ^    

In ereditarietà multipla, per utilizzare la sostituzione, devi passare un riferimento tale che, se interpretato come riferimento a un'istanza di un supertipo, funzionerà comunque. Ciò significa che il riferimento dovrebbe puntare a un altro vtable nel layout di memoria dell'intera istanza. Questo è fatto semplicemente aggiungendo un offset, fino a quando il nuovo riferimento punta al nuovo vtable. Questo offset è noto al momento della compilazione ed è probabilmente la dimensione totale dei dati precedenti nel layout.

Type C : A, B [vtableA, mem1, mem2, ..., vtableB, mem3, mem4,...]
B* from C                                ^
              <----memory size of A---->

Si noti che negli schemi ho usato "type", non "class". Se si presume che la lingua non consenta di dichiarare i propri membri dei dati, tutto quanto sopra rimane valido. L'unica differenza è che Type B ora sarà composto solo da un vtable (perché il linguaggio non consentirà mem3 , mem4 , ecc.). Ma il meccanismo è lo stesso.

    
risposta data 31.12.2014 - 11:28
fonte
1

In C ++ puoi ereditare da tipi che non condividono antenato comune ed evitare del tutto il problema del "temuto diamante". In effetti questo è ciò che fa la maggior parte del codice C ++: progettare il problema. Quando non ci sono altre opzioni, allora si può scegliere tra ereditarietà virtuale o non virtuale. Ognuno ha i suoi vantaggi e svantaggi, ma nessuno è una soluzione veramente "indirizzata con successo".

In Java, ogni classe eredita Object implicitamente. Ora questo significa che ogni eredità multipla creerà immediatamente il "diamante temuto"! Il che significa che anche una semplice gerarchia si trasformerebbe in un incubo obbligatorio. È qui che il concetto di interfaccia viene in soccorso. In C ++ interface è solo un'altra classe. In Java, interface è univoco per non che eredita da Object . Java risolve il problema del "diamante temuto" nello stesso modo in cui lo fa il 99% del codice C ++: non permettendo che i diamanti si verifichino in primo luogo.

In conclusione: l'introduzione dell'ereditarietà del diamante in Java potrebbe aiutare in pochissimi casi a un costo enorme di consentire un codice molto difficile da comprendere .

Devi tenere a mente che il vantaggio più grande che Java mantiene su C ++ è che non consente ai programmatori di giocare con funzioni pericolose che sono per lo più inutili (in gergo "spararsi al piede"). Non chiedere "perché non abbiamo X", chiediti "quali tipi di problemi non possono essere risolti senza X". La maggior parte delle volte otterresti una risposta semplice: "nessuno". O "nessuno che importi per la stragrande maggioranza degli utenti Java".

    
risposta data 31.12.2014 - 14:57
fonte
0

Come altri hanno già detto, interrogherò se C ++ ha affrontato con successo l'ereditarietà multipla.

(L'unica lingua che ho visto che I considera l'indirizzamento ereditato con successo è Eiffel , ma visto che non tutti la usiamo, è discutibile se fosse andata a buon fine. Prevedo che la maggior parte dei programmatori troverà il modo in cui l'eredità multipla è troppo difficile da imparare in quanto ci vogliono più di 10 minuti a pensare per capirlo.)

Comunque tornando a Java (e C #), in entrambi i casi era un requisito che le classi potevano essere caricate in tempo di esecuzione . E non è noto perché implementare in modo efficiente più eredità se le classi possono essere caricate dinamicamente .

Ricordo dal momento in cui Java uscì che questo era il motivo più comune dato dalle persone che ci lavoravano durante i colloqui.

    
risposta data 31.12.2014 - 11:48
fonte

Leggi altre domande sui tag