Perché non esiste una classe base in C ++?


84

Dal punto di vista del design, perché in C ++ non esiste una classe base madre di tutti, cosa succede di solito objectnegli altri linguaggi?


27
Questa è davvero più una questione filosofica che una questione di design. La risposta breve è "perché Bjarne apparentemente non voleva farlo".
Jerry Coffin

22
Personalmente penso che sia un difetto di progettazione avere tutto derivato dalla stessa classe base. Promuove uno stile di programmazione che causa più problemi di quanti ne valga la pena (dato che si tende ad avere contenitori generici di oggetti da cui è necessario ricorrere a casi per utilizzare l'oggetto reale (questo mi sembra un cattivo design IMHO)). Voglio davvero un container che possa ospitare sia auto che comandare oggetti di automazione?
Martin York

3
@ Martin ma guardala in questo modo: ad esempio, fino a C ++ 0x auto, dovevi usare definizioni di tipo lunghe un miglio per gli iteratori, o una volta typedef. Con una gerarchia di classi più generica, potresti semplicemente usare objecto iterator.
slezica

9
@Santiago: L'uso di un sistema di tipi unificato significa quasi sempre che si finisce con un sacco di codice che si basa su polimorfismo, invio dinamico e RTTI, che sono tutti relativamente costosi e che inibiscono tutte le ottimizzazioni possibili quando non vengono utilizzati.
James McNellis

2
@ GWW - ... a parte il fatto che non si applicherà a nessuno dei contenitori STL.
Chris Lutz

Risposte:


116

La sentenza definitiva si trova nelle FAQ di Stroustrup . In breve, non trasmette alcun significato semantico. Avrà un costo. I modelli sono più utili per i contenitori.

Perché C ++ non ha un oggetto di classe universale?

  • Non ne abbiamo bisogno: la programmazione generica fornisce alternative staticamente sicure nella maggior parte dei casi. Altri casi vengono gestiti utilizzando l'ereditarietà multipla.

  • Non esiste una classe universale utile: un vero universale non ha una semantica propria.

  • Una classe "universale" incoraggia a pensare in modo sciatto a tipi e interfacce e porta a un controllo del tempo di esecuzione eccessivo.

  • L'uso di una classe base universale implica un costo: gli oggetti devono essere allocati nell'heap per essere polimorfici; ciò implica memoria e costi di accesso. Gli oggetti heap non supportano naturalmente la semantica della copia. Gli oggetti heap non supportano un comportamento con ambito semplice (che complica la gestione delle risorse). Una classe base universale incoraggia l'uso di dynamic_cast e altri controlli in fase di esecuzione.


83
Non abbiamo bisogno di alcun oggetto / classe base / non abbiamo bisogno di nessun / controllo del pensiero ...;)
Piskvor ha lasciato l'edificio il

3
@Piskvor - LOL Voglio vedere il video musicale!
Byron Whitlock

11
Objects must be heap-allocated to be polymorphic- Non credo che questa affermazione sia generalmente corretta. Puoi sicuramente creare un'istanza di una classe polimorfica sullo stack e passarla come puntatore a una delle sue classi base, ottenendo un comportamento polimorfico.
Byron

5
In Java, il tentativo di eseguire il cast di un riferimento a Fooin un riferimento a Barquando Barnon eredita Foo, genera in modo deterministico un'eccezione. In C ++, il tentativo di lanciare un pointer-to-Foo in un pointer-to-Bar potrebbe mandare un robot indietro nel tempo per uccidere Sarah Connor.
supercat

3
@supercat Ecco perché usiamo dynamic_cast <>. Per evitare SkyNet e divani Chesterfield che appaiono misteriosamente.
Captain Giraffe

44

Pensiamo prima al motivo per cui vorresti avere una classe base in primo luogo. Posso pensare a diversi motivi:

  1. Per supportare operazioni o raccolte generiche che funzioneranno su oggetti di qualsiasi tipo.
  2. Includere varie procedure comuni a tutti gli oggetti (come la gestione della memoria).
  3. Tutto è un oggetto (niente primitive!). Alcuni linguaggi (come Objective-C) non lo hanno, il che rende le cose piuttosto complicate.

Questi sono i due buoni motivi per cui i linguaggi del marchio Smalltalk, Ruby e Objective-C hanno classi base (tecnicamente, Objective-C non ha realmente una classe base, ma a tutti gli effetti, lo fa).

Per # 1, la necessità di una classe base che unifica tutti gli oggetti in un'unica interfaccia è ovviata dall'inclusione di modelli in C ++. Per esempio:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

non è necessario, quando è possibile mantenere l'integrità del tipo fino in fondo per mezzo del polimorfismo parametrico!

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

Per # 2, mentre in Objective-C le procedure di gestione della memoria fanno parte dell'implementazione di una classe e sono ereditate dalla classe base, la gestione della memoria in C ++ viene eseguita utilizzando la composizione anziché l'ereditarietà. Ad esempio, puoi definire un wrapper puntatore intelligente che eseguirà il conteggio dei riferimenti su oggetti di qualsiasi tipo:

template <class T>
struct refcounted
{
  refcounted(T* object) : _object(object), _count(0) {}

  T* operator->() { return _object; }
  operator T*() { return _object; }

  void retain() { ++_count; }

  void release()
  {
    if (--_count == 0) { delete _object; }
  }

  private:
    T* _object;
    int _count;
};

Quindi, invece di chiamare metodi sull'oggetto stesso, dovresti chiamare metodi nel suo wrapper. Questo non solo consente una programmazione più generica: consente anche di separare le preoccupazioni (poiché idealmente, il tuo oggetto dovrebbe essere più preoccupato di ciò che dovrebbe fare, piuttosto che di come la sua memoria dovrebbe essere gestita in diverse situazioni).

Infine, in un linguaggio che ha sia primitive che oggetti reali come C ++, i vantaggi di avere una classe base (un'interfaccia coerente per ogni valore) vengono persi, poiché allora si hanno determinati valori che non possono essere conformi a quell'interfaccia. Per poter usare le primitive in quel tipo di situazione, devi sollevarle in oggetti (se il tuo compilatore non lo farà automaticamente). Questo crea molte complicazioni.

Quindi, la risposta breve alla tua domanda: C ++ non ha una classe base perché, avendo il polimorfismo parametrico attraverso i modelli, non ne ha bisogno.


Va bene! Sono contento che tu l'abbia trovato utile :)
Jonathan Sterling

2
Per il punto 3, contrasto con C #. C # ha primitive che possono essere inscatolate in object( System.Object), ma non è necessario che lo siano. Per il compilatore, inte System.Int32sono alias e possono essere usati in modo intercambiabile; il runtime gestisce la boxe quando necessario.
Cole Johnson,

In C ++, non c'è bisogno di boxe. Con i modelli, il compilatore genera il codice corretto in fase di compilazione.
Rob K

2
Si noti che mentre questa (vecchia) risposta dimostra un puntatore intelligente con conteggio dei riferimenti, C ++ 11 ora ha std::shared_ptrquale dovrebbe essere usato invece.

15

Il paradigma dominante per le variabili C ++ è il valore di passaggio, non il riferimento di passaggio. Forzare tutto a derivare da una radice Objectrenderebbe il passaggio per valore un errore ipse facto.

(Perché accettare un oggetto per valore come parametro, per definizione lo taglierebbe e rimuoverebbe la sua anima).

Questo non è gradito. C ++ ti fa pensare se volevi valore o semantica di riferimento, dandoti la scelta. Questa è una cosa importante nel performance computing.


1
Non sarebbe un errore da solo. Le gerarchie dei tipi esistono già e utilizzano abbastanza bene il valore di passaggio quando appropriato. Tuttavia incoraggerebbe più strategie basate sull'heap in cui il pass-by-reference è più appropriato.
Dennis Zickefoose

IMHO, un buon linguaggio dovrebbe avere tipi distinti di ereditarietà per le situazioni in cui una classe derivata può essere legittimamente utilizzata come oggetto di classe base usando slice-by-reference, quelle in cui può essere trasformata in uno usando slice-by-value, e quelli in cui nessuno dei due è legittimo. Il valore di avere un tipo di base comune per le cose che possono essere affettate sarebbe limitato, ma molte cose non possono essere tagliate e devono essere memorizzate indirettamente; il costo aggiuntivo di far derivare tali cose da un comune Objectsarebbe relativamente basso .
supercat

5

Il problema è che esiste un tale tipo in C ++! Lo è void. :-) È possibile eseguire il cast implicitamente di qualsiasi puntatore void *, inclusi i puntatori a tipi di base, classi senza tabella virtuale e classi con tabella virtuale.

Poiché dovrebbe essere compatibile con tutte quelle categorie di oggetti, esso voidstesso non può contenere metodi virtuali. Senza funzioni virtuali e RTTI, non è possibile ottenere informazioni utili sul tipo void(corrisponde a OGNI tipo, quindi può dire solo cose che sono vere per OGNI tipo), ma le funzioni virtuali e RTTI renderebbero i tipi semplici molto inefficaci e impedirebbero a C ++ di essere un linguaggio adatto per la programmazione di basso livello con accesso diretto alla memoria ecc.

Quindi, c'è questo tipo. Fornisce solo un'interfaccia molto minimalista (in effetti vuota) a causa della natura di basso livello del linguaggio. :-)


I tipi di puntatore e i tipi di oggetto non sono gli stessi. Non puoi avere un oggetto di tipo void.
Kevin Panko

1
In effetti, è così che di solito funziona l'ereditarietà in C ++. Una delle cose più importanti che fa è la compatibilità dei tipi di puntatore e riferimento: dove è richiesto un puntatore a una classe, può essere fornito un puntatore a una classe derivata. E la capacità di puntatori static_cast tra due tipi è un segno che sono collegati in una gerarchia. Da questo punto di vista, void è sicuramente una classe base universale in C ++.
Ellioh

Se dichiari una variabile di tipo, voidnon verrà compilata e otterrai questo errore . In Java potresti avere una variabile di tipo Objecte funzionerebbe. Questa è la differenza tra voide un tipo "reale". Forse è vero che voidè il tipo base per tutto, ma poiché non fornisce alcun costruttore, metodo o campo, non c'è modo di dire che sia presente o meno. Questa affermazione non può essere provata o confutata.
Kevin Panko

1
In Java ogni variabile è un riferimento. Questa è la differenza. :-) (A proposito, in C ++ ci sono anche classi astratte che non puoi usare per dichiarare una variabile). E nella mia risposta ho spiegato perché non è possibile ottenere RTTI dal nulla. Quindi, teoricamente void still è una classe base per tutto. static_cast è una prova, poiché esegue cast solo tra tipi correlati e può essere utilizzato per void. Ma hai ragione, l'assenza di accesso RTTI in void lo rende davvero molto diverso dai tipi di base nei linguaggi di livello superiore.
Ellioh

1
@Ellioh Dopo aver consultato lo standard C ++ 11 §3.9.1p9, lo ammetto void è un tipo (e scoperto un uso in? : qualche modo intelligente ), ma è ancora molto lontano dall'essere una classe base universale. Per prima cosa, è "un tipo incompleto che non può essere completato" e per un altro, non esiste alcun void&tipo.
bcrist

-2

Il C ++ è un linguaggio fortemente tipizzato. Eppure è sconcertante che non abbia un tipo di oggetto universale nel contesto della specializzazione dei modelli.

Prendi, ad esempio, il modello

template <class T> class Hook;
template <class ReturnType, class ... ArgTypes>
class Hook<ReturnType (ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

che può essere istanziato come

Hook<decltype(some_function)> ...;

Supponiamo ora di volere lo stesso per una particolare funzione. Piace

template <auto fallback> class Hook;
template <auto fallback, class ReturnType, class ... ArgTypes>
class Hook<ReturnType fallback(ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

con l'istanza specializzata

Hook<some_function> ...

Ma ahimè, anche se la classe T può sostituire qualsiasi tipo (classe o meno) prima della specializzazione, non c'è equivalente auto fallback(sto usando quella sintassi come la più ovvia generica non di tipo in questo contesto) che potrebbe sostituire qualsiasi argomento del modello non di tipo prima della specializzazione.

Quindi, in generale, questo modello non viene trasferito da argomenti di modello di tipo a argomenti di modello non di tipo.

Come con molti angoli del linguaggio C ++, la risposta è probabilmente "nessun membro del comitato ci ha pensato".


Anche se (qualcosa come) il tuo ReturnType fallback(ArgTypes...)funzionasse, sarebbe un cattivo design. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...fa quello che vuoi e significa che i parametri del modello Hooksono di tipo
Caleth

-4

Il C ++ era inizialmente chiamato "C con classi". È una progressione del linguaggio C, a differenza di altre cose più moderne come C #. E non puoi vedere il C ++ come un linguaggio, ma come un fondamento dei linguaggi (Sì, sto ricordando il libro di Scott Meyers Effective C ++).

Il C stesso è un mix di linguaggi, il linguaggio di programmazione C e il suo preprocessore.

C ++ aggiunge un altro mix:

  • l'approccio classe / oggetti

  • modelli

  • il STL

Personalmente non mi piacciono alcune cose che provengono direttamente da C a C ++. Un esempio è la funzione enum. Il modo in cui C # consente allo sviluppatore di usarlo è decisamente migliore: limita l'enumerazione nel proprio ambito, ha una proprietà Count ed è facilmente iterabile.

Poiché il C ++ voleva essere retrocompatibile con C, il designer è stato molto permissivo nel consentire al linguaggio C di entrare nella sua interezza in C ++ (ci sono alcune sottili differenze, ma non ricordo nulla che potresti fare usando un compilatore C che hai non poteva fare usando un compilatore C ++).


"Non riesco a vedere il C ++ come un linguaggio, ma come un fondamento di linguaggi" ... ti interessa spiegare e illustrare effettivamente a cosa porta quella bizzarra affermazione? I paradigmi multipli sono distinti dai "linguaggi multipli", anche se è giusto dire che il preprocessore è disgiunto dal resto dei passaggi del compilatore: buon punto. Rienumerazioni: possono essere banalmente racchiuse in un ambito di classe se lo si desidera e l'intero linguaggio manca di introspezione - un problema importante, anche se in questo caso può essere approssimato - vedere il codice benum di Boost vault. Il tuo punto è solo Q: C ++ non è iniziato con una base universale?
Tony Delroy

@ Tony: Curiosamente, il libro a cui ho fatto riferimento l'unica cosa che non menzionano nemmeno come un'altra lingua è il preprocessore, che è l'unico che consideri separatamente. Capisco il tuo punto di vista, poiché il preprocessore analizza prima il proprio input. Ma avere un meccanismo di creazione dei modelli è come avere un altro linguaggio che fa la generalizzazione per te. Applichi una sintassi del modello e avrai il compilatore che genera funzioni per ogni singolo tipo che hai. Quando puoi avere un programma scritto in C e dare il suo input a un compilatore C ++, questo è due linguaggi in uno: C e C con oggetti => C ++
sergiol

STL, può essere visto come un add-on, ma ha classi e contenitori molto pratici che attraverso i modelli estendono NATIVAMENTE la potenza del C ++. E le enumerazioni che stavo dicendo sono le enumerazioni NATIVE. Quando parli di Boost, ti affidi a codice di terze parti che non è NATIVO della lingua. In C #, non è necessario includere nulla di esterno per avere un'enumerazione iterabile e con ambito autonomo. In C ++, un trucco che uso spesso, è chiamare l'ultimo elemento di un'enumerazione - senza valori assegnati, in modo che vengano automaticamente avviati da zero e incrementati - qualcosa come NUM_ITEMS, e questo mi dà il limite superiore.
sergiol

2
grazie per la tua ulteriore spiegazione - posso almeno vedere da dove vieni; Ho sentito alcune persone lamentarsi del fatto che imparare a usare i modelli sembrava disgiunto da altri programmi C ++, anche se non è certo la mia esperienza. Lascio che altri lettori facciano quello che vogliono delle altre prospettive che presenti. Saluti.
Tony Delroy

1
Penso che il problema più grande nell'avere un tipo di base universale sia che un tale tipo sarebbe inutile se non avesse metodi virtuali; C ++ non voleva aggiungere alcun sovraccarico per i tipi di struttura che non hanno metodi virtuali, ma ogni oggetto di qualsiasi tipo che ha metodi virtuali deve avere, come minimo, un puntatore a una tabella di invio. Se classi e strutture avessero abitato universi diversi, sarebbe stato possibile avere un oggetto di classe universale, ma classi e strutture sono fondamentalmente la stessa cosa in C ++.
supercat
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.