Perché la base per tutti gli oggetti è scoraggiata in C ++


76

Stroustrup dice "Non inventare immediatamente una base unica per tutte le tue classi (una classe Object). In genere, puoi fare di meglio senza di essa per molte / la maggior parte delle classi." (Il linguaggio di programmazione C ++ Quarta edizione, Sezione 1.3.4)

Perché una classe base per tutto è generalmente una cattiva idea e quando ha senso crearne una?


16
perché C ++ non è Java ... E non dovresti provare a forzarlo.
AK_16

10

26
Inoltre, non sono d'accordo con gli stretti voti per "principalmente basato sull'opinione". Ci sono ragioni molto specifiche che possono essere spiegate per questo, poiché le risposte attestano sia su questa domanda che sulla domanda SO collegata.

2
È il principio "non ne avrai bisogno" di agile. A meno che tu non abbia già identificato una particolare necessità, non farlo (fino a quando non lo fai).
Jool,

3
@AK_: Manca un "tanto stupido quanto" nel tuo commento.
DeadMG,

Risposte:


75

Perché cosa avrebbe quell'oggetto per funzionalità? In java tutta la classe Base ha un toString, un hashCode & uguaglianza e una variabile monitor + condition.

  • ToString è utile solo per il debug.

  • hashCode è utile solo se si desidera archiviarlo in una raccolta basata su hash (la preferenza in C ++ è di passare una funzione di hashing al contenitore come parametro del modello o di evitare del std::unordered_*tutto e invece utilizzare std::vectore semplici elenchi non ordinati).

  • l'uguaglianza senza un oggetto base può essere aiutata in fase di compilazione, se non hanno lo stesso tipo non possono essere uguali. In C ++ questo è un errore di tempo di compilazione.

  • la variabile monitor e condition è meglio inclusa esplicitamente caso per caso.

Tuttavia, quando c'è altro da fare, c'è un caso d'uso.

Ad esempio in QT esiste la QObjectclasse radice che costituisce la base dell'affinità di thread, della gerarchia di proprietà padre-figlio e del meccanismo degli slot di segnale. Impone inoltre l'uso del puntatore per QObjects, tuttavia molte classi in Qt non ereditano QObject perché non hanno bisogno dello slot del segnale (in particolare i tipi di valore di una descrizione).


7
Hai dimenticato di menzionare probabilmente il motivo principale per cui Java ha una classe base: prima dei generici, le classi di raccolta avevano bisogno della classe base per funzionare. Tutto (memoria interna, parametri, valori restituiti) è stato digitato Object.
Aleksandr Dubinsky,

1
@AleksandrDubinsky: E i generici hanno aggiunto solo zucchero sintattico, non cambiando nulla ma lo smalto.
Deduplicatore,

4
Direi che il codice hash, l'uguaglianza e il supporto del monitor sono errori di progettazione anche in Java. Chi ha pensato che fosse una buona idea rendere tutti gli oggetti una serratura ?!
usr

1
Sì, ma nessuno lo vuole. Quando è stata l'ultima volta che è necessario bloccare un oggetto e non è stato possibile creare un'istanza di un oggetto di blocco separato per farlo. È molto raro e grava su tutto. I ragazzi di Java avevano una cattiva comprensione della sicurezza dei thread, come dimostrano tutti gli oggetti che sono un blocco e le raccolte thread-safe che ora sono deprecate. La sicurezza del thread è una proprietà globale, non per oggetto.
usr

2
" hashCode è utile solo se si desidera archiviarlo in una raccolta basata su hash (la preferenza in C ++ è per gli elenchi std :: vector e semplici non ordinati). " Il vero oggetto secondario _hashCodenon è "usare un contenitore diverso" ma piuttosto indicare che C ++ std::unordered_mapesegue l'hashing usando un argomento template, invece di richiedere la classe element stessa per fornire l'implementazione. Cioè, come tutti gli altri buoni contenitori e gestori di risorse in C ++, non è invadente; non inquina tutti gli oggetti con funzioni o dati nel caso in cui qualcuno possa averne bisogno in un determinato contesto in seguito.
underscore_d

100

Perché non ci sono funzioni condivise da tutti gli oggetti. Non c'è niente da mettere in questa interfaccia che avrebbe senso per tutte le classi.


10
+1 per semplicità della risposta, questa è davvero l'unica ragione.
BWG

7
Nell'ampio framework con cui ho esperienza, la classe base comune fornisce l'infrastruttura di serializzazione e riflessione desiderata nel contesto <whatever>. Meh. Ne è risultata solo la gente che serializzava un mucchio di cruft insieme a dati e metadati e rendeva il formato dei dati troppo grande e complesso per essere efficiente.
dmckee,

19
@dmckee: direi anche che la serializzazione e la riflessione non sono bisogni universalmente utili.
DeadMG

16
@DeadMG: "MA COSA SE BISOGNA SALVARE TUTTO?"
Deworde,

8
Non lo so, lo metti tra virgolette, usi tutte le maiuscole e la gente non riesce a vedere la battuta. @MSalters: Beh, dovrebbe essere facile, ha un minimo stato, basta specificare che è lì. Posso scrivere il mio nome in un elenco senza entrare in un ciclo ricorsivo.
Deworde,

25

Ogni volta che costruisci gerarchie di oggetti con ereditarietà elevata, tendi a imbatterti nel problema della Fragile Base Class (Wikipedia.) .

Avere molte piccole gerarchie di eredità separate (distinte, isolate) riduce le possibilità di incorrere in questo problema.

Rendere tutti i tuoi oggetti parte di un'unica gerarchia di eredità enorme garantisce praticamente che stai per imbatterti in questo problema.


6
Quando la classe base (in Java "java.lang.Object") non contiene alcun metodo che chiama altri metodi, il problema con la classe base fragile non può verificarsi.
Martin Rosenau,

3
Una potente classe base utile che sarebbe!
Mike Nakis,

9
@MartinRosenau ... come puoi fare in C ++ senza bisogno di una master base class!
gbjbaanb,

5
@ DavorŽdralo Quindi C ++ ha un nome stupido per una funzione di base ("operatore <<" invece di qualcosa di sensato come "DebugPrint"), mentre Java ha un mostro di una classe di base per ogni classe che scrivi, senza eccezioni. Penso che mi piace di più la verruca del C ++.
Sebastian Redl,

4
@ DavorŽdralo: il nome della funzione è irrilevante. Immagine di una sintassi cout.print(x).print(0.5).print("Bye\n")- non dipende da operator<<.
Saluti

24

Perché:

  1. Non dovresti pagare per ciò che non usi.
  2. Queste funzioni hanno meno senso in un sistema di tipi basato sul valore che in un sistema di tipi basato sul riferimento.

L'implementazione di qualsiasi tipo di virtualfunzione introduce una tabella virtuale, che richiede un sovraccarico di spazio per oggetto che non è né necessario né desiderato in molte (la maggior parte?) Situazioni.

L'implementazione non toStringvirtuale sarebbe piuttosto inutile, perché l'unica cosa che potrebbe restituire è l'indirizzo dell'oggetto, che è molto ostile per l'utente e al quale il chiamante ha già accesso, a differenza di Java.
Allo stesso modo, un non virtuale equalso hashCodepotrebbe usare solo gli indirizzi per confrontare gli oggetti, che è di nuovo piuttosto inutile e spesso addirittura completamente sbagliato - a differenza di Java, gli oggetti vengono copiati frequentemente in C ++, e quindi distinguere l'identità di un oggetto non è nemmeno sempre significativo o utile. (es. un intdavvero non dovrebbe avere un'identità diversa dal suo valore ... due numeri interi dello stesso valore dovrebbero essere uguali.)


In relazione a questo problema e al fragile problema della classe di base notato da Mike Nakis, nota l' interessante ricerca / proposta di risolverlo in Java fondamentalmente rendendo tutti i metodi internamente (cioè quando chiamati dalla stessa classe) non virtuali ma mantenendo il loro comportamento virtuale quando chiamato esternamente; per ottenere un comportamento vecchio / standard (cioè virtuale ovunque) la proposta ha introdotto una nuova openparola chiave. Non penso che sia andato oltre i documenti.
Fizz,

Un altro po 'di discussione su questo articolo è disponibile su lambda-the-ultimate.org/classic/message12271.html
Fizz,

Avere una classe base comune consentirebbe di testarne una shared_ptr<Foo> per vedere se è anche una shared_ptr<Bar>(o similmente con altri tipi di puntatore), anche se Fooe Barsono classi non correlate che non si conoscono a vicenda. Richiedere che una cosa del genere funzioni con "puntatori non elaborati", data la storia di come tali cose vengono utilizzate, sarebbe costoso, ma per le cose che verranno comunque archiviate, il costo aggiunto sarebbe minimo.
supercat,

Anche se potrebbe non essere utile avere una classe base comune per tutto, penso che ci siano alcune categorie piuttosto grandi di oggetti per le quali le classi base comuni sarebbero utili. Ad esempio, molte classi (una sostanziale pluralità se non una maggioranza) in Java possono essere utilizzate in due modi: come titolare non condiviso di dati mutabili o come titolare condivisibile di dati che nessuno è autorizzato a modificare. Con entrambi i modelli di utilizzo, un puntatore gestito (riferimento) viene utilizzato come proxy per i dati sottostanti. Essere in grado di avere un tipo di puntatore gestito comune per tutti questi dati è utile.
supercat,

16

Avere un oggetto root limita ciò che puoi fare e ciò che il compilatore può fare, senza molto guadagno.

Una classe radice comune consente di creare contenitori di qualsiasi cosa ed estrarre ciò che sono con un dynamic_cast, ma se hai bisogno di contenitori di qualsiasi cosa, allora qualcosa di simile boost::anypuò farlo senza una classe radice comune. E boost::anysupporta anche primitive - si può anche sostenere la piccola ottimizzazione buffer e lasciarle quasi "unboxed" in Java gergo.

C ++ supporta e prospera sui tipi di valore. Entrambi i letterali e i programmatori hanno scritto tipi di valore. I contenitori C ++ memorizzano, ordinano, hash, consumano e producono in modo efficiente tipi di valore.

L'ereditarietà, in particolare il tipo di eredità monolitica che implicano le classi base in stile Java, richiede tipi di "puntatore" o "riferimento" basati su negozio libero. Il tuo handle / pointer / riferimento ai dati contiene un puntatore all'interfaccia della classe e polimorficamente potrebbe rappresentare qualcos'altro.

Sebbene ciò sia utile in alcune situazioni, una volta che ti sei sposato con il modello con una "classe base comune", hai bloccato l'intera base di codice nel costo e nel bagaglio di questo modello, anche quando non è utile.

Quasi sempre sai di più su un tipo che su "è un oggetto" nel sito chiamante o nel codice che lo utilizza.

Se la funzione è semplice, scrivere la funzione come modello fornisce un polimorfismo basato sul tempo di compilazione di tipo duck in cui le informazioni sul sito chiamante non vengono eliminate. Se la funzione è più complessa, è possibile eseguire la cancellazione del tipo per cui le operazioni uniformi sul tipo che si desidera eseguire (ad esempio serializzazione e deserializzazione) possono essere costruite e memorizzate (in fase di compilazione) per essere consumate (in fase di esecuzione) dal codice in un'altra unità di traduzione.

Supponiamo di avere una libreria in cui vuoi che tutto sia serializzabile. Un approccio è quello di avere una classe base:

struct serialization_friendly {
  virtual void write_to( my_buffer* ) const = 0;
  virtual void read_from( my_buffer const* ) = 0;
  virtual ~serialization_friendly() {}
};

Ora ogni bit di codice che scrivi può essere serialization_friendly.

void serialize( my_buffer* b, serialization_friendly const* x ) {
  if (x) x->write_to(b);
}

Tranne non un std::vector, quindi ora è necessario scrivere ogni contenitore. E non quegli interi che hai preso da quella libreria di Bignum. E non quel tipo che hai scritto che non pensavi fosse necessario serializzare. E non a tuple, o a into a double, o a std::ptrdiff_t.

Adottiamo un altro approccio:

void write_to( my_buffer* b, int x ) {
  b->write_integer(x);
}    
template<class T,
  class=std::enable_if_t< void_t<
    std::declval<T const*>()->write_to( std::declval<my_buffer*>()
  > >
>
void write_to( my_buffer* b, T const* x ) {
  if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
  write_to( b, t );
}

che consiste, beh, nel non fare nulla, apparentemente. Tranne ora possiamo estenderlo write_tosostituendo write_tocome una funzione libera nello spazio dei nomi di un tipo o un metodo nel tipo.

Possiamo anche scrivere un po 'di codice di cancellazione del tipo:

namespace details {
  struct can_serialize_pimpl {
    virtual void write_to( my_buffer* ) const = 0;
    virtual void read_from( my_buffer const* ) = 0;
    virtual ~can_serialize_pimpl() {}
  };
}
struct can_serialize {
  void write_to( my_buffer* b ) const { pImpl->write_to(b); }
  void read_from( my_buffer const* b ) { pImpl->read_from(b); }
  std::unique_ptr<details::can_serialize_pimpl> pImpl;
  template<class T> can_serialize(T&&);
};
namespace details { 
  template<class T>
  struct can_serialize : can_serialize_pimpl {
    std::decay_t<T>* t;
    void write_to( my_buffer*b ) const final override {
      serialize( b, std::forward<T>(*t) );
    }
    void read_from( my_buffer const* ) final override {
      deserialize( b, std::forward<T>(*t) );
    }
    can_serialize(T&& in):t(&in) {}
  };
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
  std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}

e ora possiamo prendere un tipo arbitrario e inserirlo automaticamente in can_serializeun'interfaccia che ti consente di invocare serializein un secondo momento attraverso un'interfaccia virtuale.

Così:

void writer_thingy( can_serialize s );

è una funzione che accetta tutto ciò che può serializzare, anziché

void writer_thingy( serialization_friendly const* s );

e il primo, a differenza del secondo, in grado di gestire int, std::vector<std::vector<Bob>>automaticamente.

Non ci è voluto molto per scriverlo, soprattutto perché questo genere di cose è qualcosa che raramente vuoi fare, ma abbiamo acquisito la capacità di trattare qualsiasi cosa come serializzabile senza richiedere un tipo di base.

Inoltre, ora possiamo rendere std::vector<T>serializzabile come un cittadino di prima classe semplicemente ignorando write_to( my_buffer*, std::vector<T> const& )- con quel sovraccarico, può essere passato a un can_serializee la serializzazione delle cose std::vectorviene memorizzata in una tabella e vi si accede .write_to.

In breve, C ++ è abbastanza potente da poter implementare i vantaggi di una singola classe base al volo quando richiesto, senza dover pagare il prezzo di una gerarchia ereditaria forzata quando non richiesto. E i tempi in cui è richiesta la singola base (falsa o no) sono ragionevolmente rari.

Quando i tipi sono in realtà la loro identità e sai cosa sono, le opportunità di ottimizzazione abbondano. I dati vengono archiviati localmente e contigui (il che è molto importante per la compatibilità con la cache dei moderni processori), i compilatori possono facilmente capire cosa fa una determinata operazione (invece di avere un puntatore di metodo virtuale opaco su cui deve saltare, portando a un codice sconosciuto sul dall'altro lato) che consente di riordinare le istruzioni in modo ottimale e un numero inferiore di pioli circolari viene martellato in fori rotondi.


8

Ci sono molte buone risposte sopra, e il chiaro fatto che qualsiasi cosa tu possa fare con una classe base di tutti gli oggetti può essere fatto meglio in altri modi, come mostrato dalla risposta di @ ratchetfreak e i commenti su di esso sono molto importanti, ma c'è un altro motivo, che è quello di evitare la creazione di diamanti ereditariquando viene utilizzata l'ereditarietà multipla. Se avessi delle funzionalità in una classe di base universale, non appena avessi iniziato a utilizzare l'ereditarietà multipla, dovresti iniziare a specificare a quale variante di esso desideri accedere, perché potrebbe essere sovraccaricato in modo diverso in percorsi diversi della catena di ereditarietà. E la base non può essere virtuale, perché ciò sarebbe molto inefficiente (richiedere a tutti gli oggetti di avere una tabella virtuale a un costo potenzialmente enorme nell'uso della memoria e nella località). Questo sarebbe diventato un incubo logistico molto rapidamente.


1
Una soluzione al problema del diamante è che tutti i tipi che non derivano virtualmente un tipo di base tramite percorsi multipli hanno la precedenza su tutti i membri virtuali di quel tipo di base; se un tipo di base comune fosse stato incorporato nel linguaggio dall'inizio, un compilatore potrebbe generare automaticamente implementazioni predefinite legittime (anche se non necessariamente impressionanti).
supercat,

5

In effetti i primi compilatori e librerie C ++ di Microsoft (so di Visual C ++, 16 bit) avevano una tale classe chiamata CObject.

Tuttavia, devi sapere che a quel tempo i "modelli" non erano supportati da questo semplice compilatore C ++, quindi classi come quelle std::vector<class T>non erano possibili. Invece un'implementazione "vettoriale" poteva gestire solo un tipo di classe, quindi c'era una classe che è paragonabile a std::vector<CObject>oggi. Poiché CObjectera la classe di base di quasi tutte le classi (purtroppo non di CString- l'equivalente di stringnei compilatori moderni) è possibile utilizzare questa classe per archiviare quasi tutti i tipi di oggetti.

Poiché i compilatori moderni supportano i modelli, questo caso d'uso di una "classe base generica" ​​non viene più indicato.

Devi pensare al fatto che l'utilizzo di una classe di base così generica costerà (un po ') memoria e tempo di esecuzione, ad esempio nella chiamata al costruttore. Quindi ci sono degli svantaggi quando si usa una tale classe, ma almeno quando si usano i compilatori C ++ moderni non c'è quasi alcun caso d'uso per una tale classe.


3
Quello è MFC? [padding di commento]
user253751,

3
È davvero MFC. Un faro luminoso del design OO che ha mostrato al mondo come dovrebbero essere fatte le cose. Oh, aspetta ...
gbjbaanb,

4
@gbjbaanb Turbo Pascal e Turbo C ++ avevano i loro TObjectprima ancora che esistesse MFC. Non dare la colpa a Microsoft per quella parte del design, mi è sembrata una buona idea praticamente tutti in quel periodo.
hvd,

Anche prima dei modelli, provare a scrivere Smalltalk in C ++ ha prodotto risultati terribili.
JDługosz,

@hvd Tuttavia, MFC è stato un esempio molto peggiore di design orientato agli oggetti rispetto a qualsiasi cosa prodotta da Borland.
Jules,

5

Sto per suggerire un altro motivo che proviene da Java.

Perché non puoi creare una classe base per tutto almeno non senza un gruppo di piastre della caldaia.

Potresti riuscire a cavartela per le tue classi, ma probabilmente scoprirai che finirai per duplicare molto codice. Ad esempio "Non posso usare std::vectorqui poiché non implementa IObject- farei meglio a creare un nuovo derivato IVectorObjectche fa la cosa giusta ...".

Questo sarà il caso ogni volta che hai a che fare con built-in o classi di libreria standard o classi di altre librerie.

Ora, se è stato costruito nella lingua che ci si finisce con cose come il Integere intla confusione che è in java, o di un grande cambiamento per la sintassi del linguaggio. (Intendiamoci, penso che alcune altre lingue abbiano fatto un buon lavoro costruendolo in ogni tipo - ruby ​​sembra un esempio migliore.)

Si noti inoltre che se la classe di base non è polimorfica di runtime (ovvero utilizzando funzioni virtuali), si potrebbe ottenere lo stesso vantaggio dall'uso di tratti come framework.

ad esempio, invece di .toString()te, potresti avere quanto segue: (NOTA: so che puoi fare questo lavoro utilizzando le librerie esistenti ecc., è solo un esempio illustrativo.)

template<typename T>
struct ToStringTrait;

template<typename T> 
std::string toString(const T & t) {
  return ToStringTrait<T>::toString(t);
}

template<>
struct ToStringTrait<int> {
  std::string toString(int v) {
    return itoa(v);
  }
}

template<typename T>
struct ToStringTrait<std::vector<T>> {
  std::string toString(const std::vector<T> &v) {
    std::stringstream ss;
    ss<<"{";
    for(int i=0; i<v.size(); ++i) {
      ss<<toString(v[i]);
    }
    ss<<"}";
    return ss.str();
  }
}

3

Probabilmente il "vuoto" ricopre molti ruoli di una classe base universale. Puoi lanciare qualsiasi puntatore a void*. È quindi possibile confrontare tali puntatori. Puoi static_casttornare alla lezione originale.

Tuttavia ciò che non puoi fare con voidcui puoi fare Objectè usare RTTI per capire quale tipo di oggetto hai davvero. Questo in ultima analisi dipende dal modo in cui non tutti gli oggetti in C ++ hanno RTTI, e infatti è possibile avere oggetti a larghezza zero.


1
Solo oggetti secondari di base di larghezza zero, non normali.
Deduplicatore,

@Deduplicator A titolo di aggiornamento, aggiunge C ++ 17 [[no_unique_address]], che può essere utilizzato dai compilatori per dare larghezza zero agli oggetti secondari dei membri.
underscore_d

1
@underscore_d Intendi pianificato per C ++ 20, [[no_unique_address]]consentirà al compilatore di variabili membro EBO.
Deduplicatore,

@Deduplicator Whoops, yup. Ho già iniziato a utilizzare C ++ 17, ma credo che continui a pensare che sia più all'avanguardia di quanto non sia in realtà!
underscore_d

2

Java prende la filosofia di progettazione secondo cui Undefined Behavior non dovrebbe esistere . Codice come:

Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();

verificherà se felixcontiene un sottotipo Catdell'interfaccia dell'attrezzo Woofer; se lo fa, eseguirà il cast e invocherà woof()e, in caso contrario, genererà un'eccezione. Il comportamento del codice è completamente definito, sia che si felixattui Woofero meno .

Il C ++ prende la filosofia secondo cui se un programma non dovesse tentare qualche operazione, non dovrebbe importare cosa farebbe il codice generato se quell'operazione fosse tentata, e il computer non dovrebbe perdere tempo a cercare di limitare il comportamento nei casi che "dovrebbero" non sorgere mai. In C ++, aggiungendo gli operatori indiretti appropriati in modo da trasmettere a *Cata *Woofer, il codice produrrebbe un comportamento definito quando il cast è legittimo, ma un comportamento indefinito quando non lo è .

Avere un tipo di base comune per le cose rende possibile convalidare i cast tra i derivati ​​di quel tipo di base e anche eseguire operazioni di prova, ma la convalida dei cast è più costosa del semplice presupporre che siano legittimi e sperando che non accada nulla di male. La filosofia C ++ sarebbe che tale convalida richiede "pagare qualcosa che [di solito] non è necessario".

Un altro problema che riguarda C ++, ma non sarebbe un problema per un nuovo linguaggio, è che se più programmatori creano ciascuno una base comune, ne ricavano le proprie classi e scrivono codice per lavorare con cose di quella classe base comune, tale codice non sarà in grado di funzionare con oggetti sviluppati da programmatori che hanno utilizzato una classe base diversa. Se un nuovo linguaggio richiede che tutti gli oggetti heap abbiano un formato di intestazione comune e non abbia mai consentito oggetti heap che non lo hanno fatto, un metodo che richiede un riferimento a un oggetto heap con tale intestazione accetterà un riferimento a qualsiasi oggetto heap chiunque potrebbe mai creare.

Personalmente, penso che avere un mezzo comune per chiedere a un oggetto "sei convertibile in tipo X" sia una caratteristica molto importante in un linguaggio / framework, ma se tale caratteristica non è integrata in un linguaggio dall'inizio è difficile aggiungilo più tardi. Personalmente, penso che una classe di base del genere debba essere aggiunta a una libreria standard alla prima opportunità, con una forte raccomandazione che tutti gli oggetti che verranno utilizzati polimorficamente dovrebbero ereditare da quella base. Avere programmatori ciascuno implementare i propri "tipi di base" renderebbe più difficile il passaggio di oggetti tra codici di persone diverse, ma avere un tipo di base comune da cui molti programmatori ereditati renderebbe più semplice.

ADDENDUM

Usando i template, è possibile definire un "porta oggetti arbitrario" e chiederlo del tipo di oggetto in esso contenuto; il pacchetto Boost contiene una cosa chiamata any. Pertanto, anche se il C ++ non ha un tipo "riferimento controllabile a qualsiasi tipo" standard, è possibile crearne uno. Questo non risolve il suddetto problema con il non avere qualcosa nello standard linguistico, cioè l'incompatibilità tra le diverse implementazioni dei programmatori, ma spiega come il C ++ passa senza avere un tipo di base da cui deriva tutto: rendendo possibile la creazione qualcosa che si comporta come uno.


Quel cast fallisce in fase di compilazione in C ++ , Java e C # .
Milleniumbug,

1
@milleniumbug: se Wooferè un'interfaccia ed Catè ereditabile, il cast sarebbe legittimo perché potrebbe esistere (se non ora, possibilmente in futuro) WoofingCatche eredita Cate implementa Woofer. Si noti che in base al modello di compilazione / collegamento Java, la creazione di un WoofingCatnon richiederebbe l'accesso al codice sorgente per CatWoofer.
supercat,

3
C ++ ha dynamic_cast , che gestisce correttamente il tentativo di eseguire il cast da a Cata a Woofere risponderà alla domanda "sei convertibile in tipo X". Il C ++ ti permetterà di forzare un cast, perché ehi, forse sai davvero cosa stai facendo, ma ti aiuterà anche se non è quello che intendi veramente fare.
Rob K,

2
@RobK: hai ragione sulla sintassi ovviamente; colpa mia. Ho letto un po 'di più su dynamic_cast e sembra che in un certo senso il C ++ moderno abbia tutti gli oggetti polimorfici derivati ​​da una classe base di "oggetto polimorfico" di base con qualunque campo sia necessario per identificare il tipo di oggetto (in genere una vtable puntatore, sebbene sia un dettaglio di implementazione). Il C ++ non descrive le classi polimorfiche in quel modo, ma passare un puntatore a dynamic_castavrà un comportamento definito se punta a un oggetto polimorfico e un comportamento indefinito se non lo fa, quindi da una prospettiva semantica ...
supercat

2
... tutti gli oggetti polimorfici memorizzano alcune informazioni con lo stesso layout e tutti supportano un comportamento che non è supportato da oggetti non polimorfici; a mio avviso, ciò significa che si comportano come se derivassero da una base comune, indipendentemente dal fatto che la definizione del linguaggio utilizzi tale terminologia o meno.
supercat,

1

Symbian C ++ aveva in effetti una classe base universale, CBase, per tutti gli oggetti che si comportavano in un modo particolare (principalmente se allocavano l'heap). Ha fornito un distruttore virtuale, azzerato la memoria della classe durante la costruzione e nascosto il costruttore di copie.

La logica alla base era che si trattava di un linguaggio per sistemi embedded e compilatori C ++ e le specifiche erano davvero merda 10 anni fa.

Non tutte le classi ereditate da questo, solo alcune.

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.