Warum gibt es in C ++ keine Basisklasse?

84

Kurze Frage: Aus gestalterischer Sicht gibt es in C ++ keine Basisklasse für alle Mutter, was normalerweise objectin anderen Sprachen der Fall ist.

Slezica
quelle
27
Dies ist wirklich eher eine philosophische Frage als eine Designfrage. Die kurze Antwort lautet: "Weil Bjarne das anscheinend nicht wollte."
Jerry Coffin
4
Nichts hindert dich daran, einen zu machen.
GWW
22
Persönlich halte ich es für einen Konstruktionsfehler, wenn alles von derselben Basisklasse abgeleitet ist. Es führt zu einem Programmierstil, der mehr Probleme verursacht, als es wert ist (da Sie dazu neigen, generische Objektcontainer zu haben, aus denen Sie dann einen Fall erstellen müssen, um das eigentliche Objekt zu verwenden (dies wird meiner Meinung nach als schlechtes Design missbilligt)). Möchte ich wirklich einen Container, der sowohl Autos aufnehmen als auch Automatisierungsobjekte befehlen kann?
Martin York
3
@Martin, aber sehen Sie es so: Bis zu C ++ 0x auto mussten Sie beispielsweise meilenlange Typdefinitionen für Iteratoren oder einmalige typedefs verwenden. Mit einer allgemeineren Klassenhierarchie können Sie einfach objectoder verwenden iterator.
Slezica
9
@ Santiago: Die Verwendung eines einheitlichen Typsystems bedeutet fast immer, dass Sie am Ende viel Code haben, der auf Polymorphismus, dynamischem Versand und RTTI beruht. All dies ist relativ teuer und verhindert Optimierungen, die möglich sind, wenn sie möglich sind werden nicht verwendet.
James McNellis

Antworten:

116

Die endgültige Entscheidung finden Sie in den FAQs von Stroustrup . Kurz gesagt, es vermittelt keine semantische Bedeutung. Es wird Kosten haben. Vorlagen sind für Container nützlicher.

Warum hat C ++ kein universelles Klassenobjekt?

  • Wir brauchen keine: Generische Programmierung bietet in den meisten Fällen statisch typsichere Alternativen. Andere Fälle werden mit Mehrfachvererbung behandelt.

  • Es gibt keine nützliche universelle Klasse: Eine wirklich universelle Klasse trägt keine eigene Semantik.

  • Eine "universelle" Klasse fördert das schlampige Denken über Typen und Schnittstellen und führt zu einer übermäßigen Laufzeitprüfung.

  • Die Verwendung einer universellen Basisklasse bedeutet Kosten: Objekte müssen Heap-zugeordnet sein, um polymorph zu sein. das impliziert Speicher- und Zugriffskosten. Heap-Objekte unterstützen natürlich keine Kopiersemantik. Heap-Objekte unterstützen kein einfaches Verhalten mit Gültigkeitsbereich (was die Ressourcenverwaltung erschwert). Eine universelle Basisklasse empfiehlt die Verwendung von dynamic_cast und anderen Laufzeitprüfungen.

Kapitän Giraffe
quelle
83
Wir brauchen kein / Basisklassenobjekt / wir brauchen keine / Gedankenkontrolle ...;)
Piskvor verließ das Gebäude am
3
@Piskvor - LOL Ich möchte das Musikvideo sehen!
Byron Whitlock
10
Objects must be heap-allocated to be polymorphic- Ich denke nicht, dass diese Aussage im Allgemeinen richtig ist. Sie können definitiv eine Instanz einer polymorphen Klasse auf dem Stapel erstellen und als Zeiger auf eine ihrer Basisklassen weitergeben, was zu einem polymorphen Verhalten führt.
Byron
5
In Java versucht , eine Referenz-werfen Fooin einen Referenz-to- Barwenn Barnicht erben wird Foo, deterministisch eine Ausnahme auslöst. In C ++ könnte der Versuch, einen Zeiger auf Foo in einen Zeiger auf einen Balken umzuwandeln, einen Roboter rechtzeitig zurückschicken, um Sarah Connor zu töten.
Supercat
3
@supercat Deshalb verwenden wir dynamic_cast <>. Um SkyNet und mysteriös erscheinende Chesterfield-Sofas zu vermeiden.
Kapitän Giraffe
44

Lassen Sie uns zunächst darüber nachdenken, warum Sie überhaupt eine Basisklasse haben möchten. Ich kann mir ein paar verschiedene Gründe vorstellen:

  1. Unterstützung generischer Operationen oder Sammlungen, die für Objekte eines beliebigen Typs geeignet sind.
  2. Um verschiedene Verfahren einzuschließen, die allen Objekten gemeinsam sind (z. B. Speicherverwaltung).
  3. Alles ist ein Objekt (keine Grundelemente!). Einige Sprachen (wie Objective-C) haben dies nicht, was die Dinge ziemlich chaotisch macht.

Dies sind die beiden guten Gründe, warum Sprachen der Marken Smalltalk, Ruby und Objective-C Basisklassen haben (technisch gesehen hat Objective-C keine Basisklasse, aber in jeder Hinsicht).

Für # 1 wird die Notwendigkeit einer Basisklasse, die alle Objekte unter einer einzigen Schnittstelle vereint, durch die Aufnahme von Vorlagen in C ++ vermieden. Zum Beispiel:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

ist unnötig, wenn Sie die Typintegrität durch parametrischen Polymorphismus vollständig aufrechterhalten können!

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

Während in Objective-C Speicherverwaltungsprozeduren Teil der Implementierung einer Klasse sind und von der Basisklasse geerbt werden, wird die Speicherverwaltung in C ++ eher durch Komposition als durch Vererbung durchgeführt. Sie können beispielsweise einen Smart Pointer Wrapper definieren, der die Referenzzählung für Objekte eines beliebigen Typs durchführt:

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;
};

Anstatt Methoden für das Objekt selbst aufzurufen, würden Sie dann Methoden in seinem Wrapper aufrufen. Dies ermöglicht nicht nur eine allgemeinere Programmierung, sondern ermöglicht es Ihnen auch, Bedenken zu trennen (da Ihr Objekt im Idealfall mehr darüber nachdenken sollte, was es tun sollte, als darüber, wie sein Speicher in verschiedenen Situationen verwaltet werden sollte).

Schließlich in einer Sprache, die sowohl Grundelemente als auch tatsächliche Objekte wie C ++ enthält, die Vorteile einer Basisklasse (eine konsistente Schnittstelle für alle Wert) verloren, da Sie dann bestimmte Werte haben, die dieser Schnittstelle nicht entsprechen können. Um Primitive in einer solchen Situation zu verwenden, müssen Sie sie in Objekte heben (wenn Ihr Compiler dies nicht automatisch tut). Dies verursacht viele Komplikationen.

Die kurze Antwort auf Ihre Frage: C ++ hat keine Basisklasse, da dies aufgrund des parametrischen Polymorphismus durch Vorlagen nicht erforderlich ist.

Jonathan Sterling
quelle
Es ist in Ordnung! Ich bin froh, dass du es hilfreich fandest :)
Jonathan Sterling
2
Für Punkt 3 kontere ich mit C #. C # hat Primitive , die kann zu geboxt werden object( System.Object), aber nicht sein müssen. Für den Compiler intund System.Int32sind Aliase und können austauschbar verwendet werden; Die Laufzeit übernimmt das Boxen bei Bedarf.
Cole Johnson
In C ++ ist kein Boxen erforderlich. Bei Vorlagen generiert der Compiler zur Kompilierungszeit den richtigen Code.
Rob K
2
Bitte beachten Sie, dass diese (alte) Antwort zwar einen intelligenten Zeiger mit Referenzzählung zeigt, C ++ 11 nun jedoch einen Zeiger hat std::shared_ptr, der stattdessen verwendet werden sollte.
15

Das vorherrschende Paradigma für C ++ - Variablen ist Pass-by-Value, nicht Pass-by-Ref. Erzwingen, dass alles von einer Wurzel abgeleitet wirdObject würde das Übergeben als Wert ipse facto zu einem Fehler machen.

(Weil das Akzeptieren eines Objekts nach Wert als Parameter es per Definition in Scheiben schneiden und seine Seele entfernen würde).

Das ist nicht erwünscht. In C ++ überlegen Sie, ob Sie eine Wert- oder Referenzsemantik wünschen, und haben die Wahl. Dies ist eine große Sache im Performance Computing.

sehe sehen
quelle
1
Es wäre kein Fehler für sich. Es gibt bereits Typhierarchien, die bei Bedarf die Übergabe von Werten recht gut nutzen. Es würde jedoch mehr Heap-basierte Strategien fördern, bei denen eine Referenzübergabe angemessener ist.
Dennis Zickefoose
IMHO sollte eine gute Sprache unterschiedliche Arten der Vererbung für Situationen haben, in denen eine abgeleitete Klasse legitimerweise als Basisklassenobjekt unter Verwendung von Slice-by-Reference verwendet werden kann, und solche, bei denen sie mithilfe von Slice-by-Value in ein Objekt umgewandelt werden kann. und diejenigen, bei denen beides nicht legitim ist. Der Wert eines gemeinsamen Basistyps für Dinge, die in Scheiben geschnitten werden können, wäre begrenzt, aber viele Dinge können nicht geschnitten werden und müssen indirekt gespeichert werden. Die zusätzlichen Kosten für solche Dinge Objectwären relativ gering.
Supercat
6

Das Problem ist, dass es in C ++ einen solchen Typ gibt! Es ist void. :-) Jeder Zeiger kann sicher implizit auf geworfen werdenvoid * , einschließlich Zeiger auf Basistypen, Klassen ohne virtuelle Tabelle und Klassen mit virtueller Tabelle.

Da es mit all diesen Objektkategorien kompatibel sein sollte, voidkann es selbst keine virtuellen Methoden enthalten. Ohne virtuelle Funktionen und RTTI können keine nützlichen Informationen zum Typ abgerufen werden void(sie stimmen mit JEDEM Typ überein, können also nur Dinge sagen, die für JEDEN Typ zutreffen), aber virtuelle Funktionen und RTTI würden einfache Typen sehr unwirksam machen und C ++ daran hindern, zu sein Eine Sprache, die für Low-Level-Programmierung mit direktem Speicherzugriff usw. geeignet ist.

Es gibt also einen solchen Typ. Es bietet nur eine sehr minimalistische (tatsächlich leere) Oberfläche, da die Sprache nur ein niedriges Niveau aufweist. :-)

Ellioh
quelle
Zeigertypen und Objekttypen sind nicht identisch. Sie können kein Objekt vom Typ haben void.
Kevin Panko
1
In der Tat funktioniert die Vererbung in C ++ normalerweise so. Eines der wichtigsten Dinge, die es tut, ist die Kompatibilität von Zeiger- und Referenztypen: Wenn ein Zeiger auf eine Klasse erforderlich ist, kann ein Zeiger auf eine abgeleitete Klasse angegeben werden. Die Fähigkeit, Zeiger zwischen zwei Typen statisch zu übertragen, ist ein Zeichen dafür, dass sie in einer Hierarchie verbunden sind. Unter diesem Gesichtspunkt ist void definitiv eine universelle Basisklasse in C ++.
Ellioh
Wenn Sie eine Variable vom Typ deklarieren void, wird sie nicht kompiliert und Sie erhalten diesen Fehler . In Java könnten Sie eine Variable vom Typ haben Objectund es würde funktionieren. Das ist der Unterschied zwischen voidund einem "echten" Typ. Vielleicht ist es wahr, dass dies voidder Basistyp für alles ist, aber da es keine Konstruktoren, Methoden oder Felder enthält, gibt es keine Möglichkeit zu sagen, ob es da ist oder nicht. Diese Behauptung kann nicht bewiesen oder widerlegt werden.
Kevin Panko
1
In Java ist jede Variable eine Referenz. Das ist der Unterschied. :-) (Übrigens gibt es in C ++ auch abstrakte Klassen, mit denen Sie keine Variable deklarieren können). Und in meiner Antwort erklärte ich, warum es nicht möglich ist, RTTI aus dem Nichts zu bekommen. Theoretisch ist void immer noch eine Basisklasse für alles. static_cast ist ein Beweis, da es nur zwischen verwandten Typen Casts ausführt und für void verwendet werden kann. Aber Sie haben Recht, das Fehlen eines RTTI-Zugriffs in void unterscheidet sich wirklich stark von den Basistypen in den höheren Sprachen.
Ellioh
1
@Ellioh Nachdem ich den C ++ 11-Standard §3.9.1p9 konsultiert habe, gebe ich zu, dass dies voidein Typ ist (und eine etwas clevere Verwendung entdeckt hat? : ), aber es ist noch weit davon entfernt, eine universelle Basisklasse zu sein. Zum einen ist es "ein unvollständiger Typ, der nicht vervollständigt werden kann", und zum anderen gibt es keinen void&Typ.
bcrist
-2

C ++ ist eine stark typisierte Sprache. Es ist jedoch verwunderlich, dass es im Rahmen der Vorlagenspezialisierung keinen universellen Objekttyp gibt.

Nehmen Sie zum Beispiel das Muster

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

was als instanziiert werden kann

Hook<decltype(some_function)> ...;

Nehmen wir nun an, wir wollen dasselbe für eine bestimmte Funktion. Mögen

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

mit der spezialisierten Instanziierung

Hook<some_function> ...

Aber leider gibt es, obwohl Klasse T vor der Spezialisierung für jeden Typ (Klasse oder nicht) stehen kann, kein Äquivalent auto fallback(ich verwende diese Syntax als die offensichtlichste generische Nicht-Typ-Syntax in diesem Zusammenhang), das für jeden stehen könnte Nicht typisiertes Vorlagenargument vor der Spezialisierung.

Im Allgemeinen wird dieses Muster also nicht von Typvorlagenvorlagenargumenten auf Nicht-Typvorlagenvorlagenargumente übertragen.

Wie bei vielen Ecken in der C ++ - Sprache lautet die Antwort wahrscheinlich "kein Ausschussmitglied hat daran gedacht".

user7339377
quelle
Selbst wenn (so etwas wie) ReturnType fallback(ArgTypes...)funktionieren würde, wäre es schlechtes Design. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...tut, was Sie wollen und bedeutet, dass die Vorlagenparameter Hookvon konsistenter Art sind
Caleth
-4

C ++ wurde ursprünglich "C mit Klassen" genannt. Es ist eine Weiterentwicklung der C-Sprache, im Gegensatz zu einigen anderen moderneren Dingen wie C #. Und Sie können C ++ nicht als Sprache sehen, sondern als Grundlage von Sprachen (Ja, ich erinnere mich an das Scott Meyers-Buch Effective C ++).

C selbst ist eine Mischung aus Sprachen, der Programmiersprache C und ihrem Präprozessor.

C ++ fügt eine weitere Mischung hinzu:

  • der Klassen- / Objektansatz

  • Vorlagen

  • die STL

Ich persönlich mag einige Sachen nicht, die direkt von C nach C ++ kommen. Ein Beispiel ist die Aufzählungsfunktion. Die Art und Weise, wie C # es dem Entwickler ermöglicht, es zu verwenden, ist viel besser: Es begrenzt die Aufzählung in seinem eigenen Bereich, hat eine Count-Eigenschaft und ist leicht iterierbar.

Da C ++ mit C nachträglich kompatibel sein wollte, war der Designer sehr tolerant, die C-Sprache in C ++ vollständig eingeben zu lassen (es gibt einige subtile Unterschiede, aber ich erinnere mich an nichts, was Sie mit einem C-Compiler tun könnten, den Sie verwenden konnte nicht mit einem C ++ - Compiler tun).

Sergiol
quelle
"Kann C ++ nicht als Sprache sehen, sondern als Grundlage von Sprachen" ... möchten Sie tatsächlich erklären und veranschaulichen, worauf diese bizarre Aussage abzielt? Mehrere Paradigmen unterscheiden sich von "mehreren Sprachen", obwohl es fair ist zu sagen, dass der Präprozessor vom Rest der Compiler-Schritte getrennt ist - ein guter Punkt. Re enums - sie können auf Wunsch trivial in einen Klassenbereich eingeschlossen werden, und der gesamten Sprache fehlt die Selbstbeobachtung - ein Hauptproblem, obwohl es in diesem Fall angenähert werden kann - siehe Benum-Code von Boost Vault. Ist Ihr Punkt re Q nur: C ++ nicht startet mit einer Universal - Basis?
Tony Delroy
@ Tony: Seltsamerweise ist das Buch, auf das ich mich bezog, das einzige, was sie nicht einmal als eine andere Sprache erwähnen, der Präprozessor, der der einzige ist, den Sie separat betrachten. Ich verstehe Ihren Standpunkt, da der Präprozessor zuerst seine eigenen Eingaben analysiert. Ein Vorlagenmechanismus ist jedoch wie eine andere Sprache, die die Verallgemeinerung für Sie übernimmt. Sie wenden eine Vorlagensyntax an und der Compiler generiert Funktionen für jeden einzelnen Typ. Wenn Sie ein Programm in C schreiben lassen und seine Eingabe an einen C ++ - Compiler übergeben können, sind dies zwei Sprachen in einer: C und C mit Objekten => C ++
Sergiol
STL kann als Add-On angesehen werden, verfügt jedoch über sehr praktische Klassen und Container, die durch Vorlagen die Leistung von C ++ NATIV erweitern. Und die Aufzählungen, die ich sagte, sind die NATIVEN Aufzählungen. Wenn Sie von Boost sprechen, verlassen Sie sich auf Code von Drittanbietern, der für die Sprache nicht NATIV ist. In C # muss ich nichts Externes einfügen, um eine iterierbare Aufzählung mit Selbstbereich zu erhalten. In C ++ besteht ein Trick, den ich häufig verwende, darin, das letzte Element einer Aufzählung aufzurufen - ohne zugewiesene Werte, sodass sie automatisch von Null gestartet und inkrementiert werden - so etwas wie NUM_ITEMS, und dies gibt mir die Obergrenze.
Sergiol
2
Vielen Dank für Ihre weitere Erklärung. Ich kann zumindest sehen, woher Sie kommen. Ich habe einige Leute beschweren hören, dass sich das Erlernen des Gebrauchs von Vorlagen von anderen C ++ - Programmen unzusammenhängend anfühlte, obwohl dies sicherlich nicht meine Erfahrung ist. Ich überlasse es anderen Lesern, aus den anderen Perspektiven, die Sie präsentieren, das zu machen, was sie wollen. Prost.
Tony Delroy
1
Ich denke, das größte Problem bei einem universellen Basistyp ist, dass ein solcher Typ nutzlos wäre, wenn er keine virtuellen Methoden hätte. C ++ wollte keinen Overhead für Strukturtypen hinzufügen, die keine virtuellen Methoden haben, aber jedes Objekt eines Typs, der virtuelle Methoden hat, muss mindestens einen Zeiger auf eine Versandtabelle haben. Wenn Klassen und Strukturen unterschiedliche Universen bewohnt hätten, wäre es möglicherweise möglich gewesen, ein universelles Klassenobjekt zu haben, aber Klassen und Strukturen sind in C ++ im Grunde dasselbe.
Supercat