Stroustrup sagt: "Erfinden Sie nicht sofort eine eindeutige Basis für alle Ihre Klassen (eine Objektklasse). In der Regel können Sie für viele / die meisten Klassen besser darauf verzichten." (Die vierte Ausgabe der C ++ - Programmiersprache, Abschnitt 1.3.4)
Warum ist eine Basisklasse für alles im Allgemeinen eine schlechte Idee, und wann ist es sinnvoll, eine zu erstellen?
c++
object-oriented
object-oriented-design
Matthew James Briggs
quelle
quelle
Antworten:
Denn was hätte das Objekt für Funktionalität? In Java besteht die Basisklasse nur aus einem toString, einem hashCode & equality und einer monitor + condition-Variablen.
ToString ist nur zum Debuggen nützlich.
hashCode ist nur nützlich, wenn Sie es in einer Hash-basierten Sammlung speichern möchten (in C ++ ist es bevorzugt, eine Hash-Funktion als Template-Parameter an den Container zu übergeben oder ganz zu vermeiden
std::unordered_*
und stattdessenstd::vector
ungeordnete Listen zu verwenden).Gleichheit ohne ein Basisobjekt kann zur Kompilierungszeit unterstützt werden. Wenn sie nicht den gleichen Typ haben, können sie nicht gleich sein. In C ++ ist dies ein Fehler bei der Kompilierung.
Die Monitor- und Bedingungsvariable wird im Einzelfall besser explizit einbezogen.
Wenn jedoch mehr zu tun ist, gibt es einen Anwendungsfall.
Zum Beispiel gibt es in QT die Root-
QObject
Klasse, die die Basis für Thread-Affinität, Parent-Child-Ownership-Hierarchie und Signal-Slots-Mechanismus bildet. Es erzwingt auch die Verwendung von Zeigern für QObjects. Allerdings erben viele Klassen in Qt kein QObject, da sie keinen Signalschlitz benötigen (insbesondere die Werttypen einiger Beschreibungen).quelle
Object
._hashCode
ist nicht ‚einen anderen Behälter verwenden‘ , sondern zeigt darauf hin, dass C ++std::unordered_map
Hashing mit einem Template-Argument durchführt, anstatt dass die Elementklasse selbst die Implementierung bereitstellen muss. Das heißt, wie bei allen anderen guten Containern und Ressourcenmanagern in C ++ ist es nicht aufdringlich. Es verschmutzt nicht alle Objekte mit Funktionen oder Daten, nur für den Fall, dass sie später in einem bestimmten Kontext benötigt werden.Weil es keine Funktionen gibt, die von allen Objekten gemeinsam genutzt werden. In diesem Interface gibt es nichts, was für alle Klassen sinnvoll wäre.
quelle
Immer wenn Sie große Vererbungshierarchien von Objekten erstellen, stoßen Sie auf das Problem der Fragile Base Class (Wikipedia.) .
Das Vorhandensein vieler kleiner separater (eindeutiger, isolierter) Vererbungshierarchien verringert die Wahrscheinlichkeit, auf dieses Problem zu stoßen.
Wenn Sie alle Ihre Objekte in eine einzige hierarchische Vererbung einbinden, ist praktisch sichergestellt, dass Sie auf dieses Problem stoßen.
quelle
cout.print(x).print(0.5).print("Bye\n")
- es hängt nicht davon aboperator<<
.Weil:
Die Implementierung jeder Art
virtual
stellt eine virtuelle Funktion-Tabelle, die Raum - Overhead pro-Objekt erfordert , die weder notwendig noch erwünscht ist in vielen ( die meisten?) Situationen.Eine nicht
toString
virtuelle Implementierung wäre ziemlich nutzlos, da das einzige, was zurückgegeben werden könnte, die Adresse des Objekts ist, die sehr benutzerfreundlich ist und auf die der Aufrufer im Gegensatz zu Java bereits Zugriff hat.In ähnlicher Weise eine nicht virtuelle
equals
oderhashCode
nur könnte Adressen verwenden , um Objekte zu vergleichen, was wiederum ziemlich nutzlos ist und oft sogar völlig falsch - anders als in Java werden Objekte häufig in C kopiert ++ und damit die „Identität“ eines Objekts unterscheidet , ist nicht einmal immer sinnvoll oder nützlich. (zB eineint
sollte wirklich keine andere Identität als ihren Wert haben ... zwei ganze Zahlen desselben Werts sollten gleich sein.)quelle
open
Schlüsselwort eingeführt. Ich glaube nicht, dass es über ein paar Papiere hinausging.shared_ptr<Foo>
zu sehen , ob es auch ein istshared_ptr<Bar>
(oder auch mit anderen Zeigertypen), auch wennFoo
undBar
nicht verwandte Klassen sind , die nichts voneinander wissen. Das Erfordernis, dass so etwas mit "rohen Zeigern" zusammenarbeitet, wäre angesichts der Geschichte, wie solche Dinge verwendet werden, teuer, aber für Dinge, die sowieso auf Halden gelagert werden, wären die zusätzlichen Kosten minimal.Ein einziges Root-Objekt schränkt die Möglichkeiten und Möglichkeiten des Compilers ein, ohne dass sich dies auszahlt.
Eine gemeinsame Root-Klasse ermöglicht es, Container von allem zu erstellen und zu extrahieren, was sie sind.
dynamic_cast
Wenn Sie Container von allem benötigen,boost::any
können Sie dies jedoch auch ohne eine gemeinsame Root-Klasse tun . Undboost::any
unterstützt auch Primitive - es kann sogar die Small-Buffer-Optimierung unterstützen und sie in Java fast "unboxed" lassen.C ++ unterstützt und lebt von Werttypen. Sowohl Literale als auch vom Programmierer geschriebene Wertetypen. In C ++ - Containern werden Werttypen effizient gespeichert, sortiert, gehasht, konsumiert und produziert.
Die Vererbung, insbesondere die Art der Java-Basisklassen mit monolithischer Vererbung, erfordert "Zeiger" - oder "Referenz" -Typen, die auf einem freien Speicher basieren. Ihr Handle / Zeiger / Verweis auf Daten enthält einen Zeiger auf die Schnittstelle der Klasse und könnte polymorph etwas anderes darstellen.
Dies ist zwar in einigen Situationen nützlich, aber nachdem Sie sich mit dem Muster mit einer "gemeinsamen Basisklasse" verheiratet haben, haben Sie Ihre gesamte Codebasis an die Kosten und das Gepäck dieses Musters gebunden, auch wenn es nicht nützlich ist.
Fast immer wissen Sie mehr über einen Typ als "es ist ein Objekt" an der aufrufenden Site oder im Code, der ihn verwendet.
Wenn die Funktion einfach ist, erhalten Sie durch Schreiben der Funktion als Vorlage einen auf der Kompilierungszeit basierenden Polymorphismus vom Typ "Ente", bei dem die Informationen am aufrufenden Standort nicht verworfen werden. Wenn die Funktion komplexer ist, kann die Typlöschung durchgeführt werden, wobei die einheitlichen Operationen für den Typ, den Sie ausführen möchten (z. B. Serialisierung und Deserialisierung), erstellt und gespeichert werden können (zur Kompilierungszeit), um von der (zur Laufzeit) verbraucht zu werden Code in einer anderen Übersetzungseinheit.
Angenommen, Sie haben eine Bibliothek, in der alles serialisierbar sein soll. Ein Ansatz besteht darin, eine Basisklasse zu haben:
Jetzt kann jedes Bit Code, das Sie schreiben, sein
serialization_friendly
.Ausgenommen nicht ein
std::vector
, so müssen Sie jetzt jeden Container schreiben. Und nicht diese ganzen Zahlen, die Sie aus dieser Bignumbibliothek erhalten haben. Und nicht dieser Typ, den Sie geschrieben haben und für den Sie keine Serialisierung für nötig hielten. Und nicht eintuple
oder einint
oder eindouble
oder einstd::ptrdiff_t
.Wir verfolgen einen anderen Ansatz:
was darin besteht, scheinbar nichts zu tun. Außer jetzt können wir erweitern,
write_to
indem wirwrite_to
als freie Funktion im Namespace eines Typs oder einer Methode im Typ überschreiben .Wir können sogar ein bisschen Löschcode schreiben:
und jetzt können wir einen beliebigen Typ nehmen und ihn automatisch in eine
can_serialize
Schnittstelle packen, mit der Sieserialize
zu einem späteren Zeitpunkt über eine virtuelle Schnittstelle zugreifen können.Damit:
ist eine Funktion, die alles akzeptiert, was serialisiert werden kann, anstatt
und die erste, im Gegensatz zu dem zweiten, damit umgehen kann
int
,std::vector<std::vector<Bob>>
automatisch.Es hat nicht viel gekostet, es zu schreiben, vor allem, weil man so etwas nur selten machen möchte, aber wir haben die Möglichkeit, alles als serialisierbar zu behandeln, ohne einen Basistyp zu benötigen.
Darüber hinaus können wir
std::vector<T>
als erstklassiger Bürger die Serialisierung durch einfaches Überschreiben vornehmen.write_to( my_buffer*, std::vector<T> const& )
Mit dieser Überladung kann sie an a übergeben werden,can_serialize
und die Serialisierung derstd::vector
wird in einer vtable gespeichert, auf die von zugegriffen wird.write_to
.Kurz gesagt, C ++ ist leistungsstark genug, um die Vorteile einer einzelnen Basisklasse bei Bedarf sofort zu implementieren, ohne den Preis einer erzwungenen Vererbungshierarchie zahlen zu müssen, wenn dies nicht erforderlich ist. Und die Zeiten, in denen die einzelne Base (gefälscht oder nicht) benötigt wird, sind einigermaßen selten.
Wenn Typen tatsächlich ihre Identität sind und Sie wissen, was sie sind, gibt es zahlreiche Optimierungsmöglichkeiten. Daten werden lokal und zusammenhängend gespeichert (was für die Cachefreundlichkeit moderner Prozessoren von großer Bedeutung ist), und Compiler können leicht nachvollziehen, was eine bestimmte Operation tut (anstatt einen undurchsichtigen virtuellen Methodenzeiger zu haben, über den sie springen muss, um unbekannten Code auf dem Computer zu erzeugen) andere Seite), mit der Anweisungen optimal neu angeordnet werden können und weniger runde Stifte in runde Löcher gehämmert werden.
quelle
Es gibt oben viele gute Antworten, und die Tatsache, dass alles, was Sie mit einer Basisklasse aller Objekte tun würden, auf andere Weise besser gemacht werden kann, wie die Antwort von @ ratchetfreak und die Kommentare dazu zeigen, ist sehr wichtig, aber Es gibt noch einen weiteren Grund, der darin besteht, die Erstellung von Erbschaftsdiamanten zu vermeidenwenn Mehrfachvererbung verwendet wird. Wenn Sie Funktionen in einer universellen Basisklasse hatten, müssen Sie, sobald Sie mit der Mehrfachvererbung begonnen haben, angeben, auf welche Variante dieser Klasse Sie zugreifen möchten, da sie in verschiedenen Pfaden der Vererbungskette unterschiedlich überladen sein kann. Die Basis kann nicht virtuell sein, da dies sehr ineffizient wäre (alle Objekte müssen über eine virtuelle Tabelle verfügen, was möglicherweise enorme Kosten für die Speichernutzung und -lokalität verursacht). Dies würde sehr schnell zu einem logistischen Albtraum werden.
quelle
Tatsächlich hatten Microsofts frühe C ++ - Compiler und -Bibliotheken (ich kenne Visual C ++ mit 16 Bit) eine solche Klasse
CObject
.Sie müssen jedoch wissen, dass "Templates" zu diesem Zeitpunkt von diesem einfachen C ++ - Compiler nicht unterstützt wurden, so dass Klassen wie
std::vector<class T>
nicht möglich waren. Stattdessen konnte eine "Vektor" -Implementierung nur einen Klassentyp verarbeiten, sodass es eine Klasse gab, die mit derstd::vector<CObject>
heutigen vergleichbar ist. DaCObject
es sich bei fast allen Klassen um die Basisklasse handelte (leider nicht vonCString
- das Äquivalent zustring
modernen Compilern), konnten Sie mit dieser Klasse fast alle Arten von Objekten speichern.Da moderne Compiler Vorlagen unterstützen, wird dieser Anwendungsfall einer "generischen Basisklasse" nicht mehr angegeben.
Sie müssen bedenken, dass die Verwendung einer solchen generischen Basisklasse (etwas) Speicher und Laufzeit kostet - zum Beispiel beim Aufruf des Konstruktors. Es gibt also Nachteile bei der Verwendung einer solchen Klasse, aber zumindest bei der Verwendung moderner C ++ - Compiler gibt es für eine solche Klasse fast keinen Anwendungsfall.
quelle
TObject
bevor es MFC überhaupt gab. Machen Sie nicht Microsoft für diesen Teil des Designs verantwortlich, es schien für so ziemlich alle zu dieser Zeit eine gute Idee zu sein.Ich werde einen anderen Grund vorschlagen, der von Java kommt.
Weil man nicht für alles eine Basisklasse schaffen kann, zumindest nicht ohne ein paar Kesselplatten.
Möglicherweise schaffen Sie es nicht für Ihre eigenen Klassen - aber Sie werden wahrscheinlich feststellen, dass Sie am Ende viel Code duplizieren. ZB "Ich kann es
std::vector
hier nicht verwenden , da es nicht implementiert wirdIObject
- ich sollte ein neues abgeleitetes erstellenIVectorObject
, das das Richtige tut ...".Dies ist immer dann der Fall, wenn Sie mit integrierten oder Standardbibliotheksklassen oder Klassen aus anderen Bibliotheken arbeiten.
Wenn es nun in die Sprache eingebaut wäre, würden Sie Dinge wie das
Integer
und dieint
Verwirrung in Java oder eine große Änderung der Sprachsyntax erleben. (Wohlgemerkt, ich denke, einige andere Sprachen haben gute Arbeit geleistet, indem sie es in jeden Typ eingebaut haben - Ruby scheint ein besseres Beispiel zu sein.)Beachten Sie auch, dass Sie den gleichen Nutzen aus der Verwendung von Merkmalen wie Framework ziehen können, wenn Ihre Basisklasse nicht polymorph zur Laufzeit ist (dh wenn Sie virtuelle Funktionen verwenden).
Beispiel: Anstelle von
.toString()
Ihnen könnten Sie Folgendes haben: (HINWEIS: Ich weiß, dass Sie dies übersichtlicher mit vorhandenen Bibliotheken usw. tun können, es ist nur ein veranschaulichendes Beispiel.)quelle
Wahrscheinlich erfüllt "void" viele Rollen einer universellen Basisklasse. Sie können einen beliebigen Zeiger auf a setzen
void*
. Sie können diese Zeiger dann vergleichen. Sie könnenstatic_cast
zur ursprünglichen Klasse zurückkehren.Doch das, was Sie nicht können mit tun ,
void
die Sie tun könnenObject
ist die Verwendung RTTI , um herauszufinden , welche Art von Objekt , das Sie wirklich haben. Dies ist letztendlich darauf zurückzuführen, dass nicht alle Objekte in C ++ RTTI haben und es tatsächlich möglich ist, Objekte mit null Breite zu haben.quelle
[[no_unique_address]]
, das von Compilern verwendet werden kann, um Member-Unterobjekten die Breite Null zuzuweisen.[[no_unique_address]]
erlaubt dem Compiler EBO Member-Variablen.Java geht von der Designphilosophie aus, dass Undefiniertes Verhalten nicht existieren sollte . Code wie:
testet, ob
felix
ein SubtypCat
dieses Interfaces vorhanden istWoofer
; Wenn dies der Fall ist, wird die Umwandlung ausgeführt und aufgerufen.woof()
Wenn dies nicht der Fall ist, wird eine Ausnahme ausgelöst. Das Verhalten des Codes ist vollständig definiert, ob erfelix
implementiert istWoofer
oder nicht .C ++ vertritt die Philosophie, dass ein Programm, das keine Operation ausführen sollte, keine Rolle spielen sollte, was der generierte Code tun würde, wenn diese Operation ausgeführt würde, und dass der Computer keine Zeit damit verschwenden sollte, das Verhalten in Fällen einzuschränken, die "sollten". stehe niemals auf. Wenn in C ++ die entsprechenden Indirektionsoperatoren hinzugefügt werden, um a
*Cat
in a umzuwandeln*Woofer
, führt der Code zu einem definierten Verhalten, wenn die Umwandlung legitim ist , undefiniertem Verhalten, wenn dies nicht der Fall ist .Ein gemeinsamer Basistyp für Dinge macht es möglich, Casts unter Derivaten dieses Basistyps zu validieren und auch Try-Cast-Operationen durchzuführen. Das Validieren von Casts ist jedoch teurer, als nur davon auszugehen, dass sie legitim sind, und zu hoffen, dass nichts Schlimmes passiert. Die C ++ - Philosophie besagt, dass für eine solche Validierung "für etwas bezahlt werden muss, das [normalerweise] nicht benötigt wird".
Ein weiteres Problem, das sich auf C ++ bezieht, aber für eine neue Sprache kein Problem darstellt, besteht darin, dass, wenn mehrere Programmierer jeweils eine gemeinsame Basis erstellen, ihre eigenen Klassen daraus ableiten und Code schreiben, um mit Dingen dieser gemeinsamen Basisklasse zu arbeiten, Ein solcher Code ist nicht in der Lage, mit Objekten zu arbeiten, die von Programmierern entwickelt wurden, die eine andere Basisklasse verwendeten. Wenn eine neue Sprache erfordert, dass alle Heap-Objekte ein gemeinsames Header-Format haben, und niemals Heap-Objekte zugelassen hat, die dies nicht getan haben, akzeptiert eine Methode, die einen Verweis auf ein Heap-Objekt mit einem solchen Header erfordert, einen Verweis auf ein beliebiges Heap-Objekt könnte jemals schaffen.
Persönlich denke ich, dass es ein sehr wichtiges Merkmal in einer Sprache / einem Framework ist, ein allgemeines Mittel zu haben, um ein Objekt zu fragen: "Sind Sie konvertierbar zu Typ X?" später hinzufügen. Persönlich denke ich, dass eine solche Basisklasse bei der ersten Gelegenheit zu einer Standardbibliothek hinzugefügt werden sollte, mit einer starken Empfehlung, dass alle Objekte, die polymorph verwendet werden, von dieser Basis erben sollten. Wenn jeder Programmierer seinen eigenen "Basistyp" implementiert, würde dies die Weitergabe von Objekten zwischen dem Code verschiedener Personen erschweren. Ein gemeinsamer Basistyp, den viele Programmierer übernommen haben, würde dies jedoch einfacher machen.
NACHTRAG
Mithilfe von Vorlagen ist es möglich, einen "beliebigen Objekthalter" zu definieren und ihn nach dem Typ des darin enthaltenen Objekts zu fragen. Das Boost-Paket enthält so etwas wie
any
. Daher ist es möglich, einen Typ zu erstellen, auch wenn C ++ keinen standardmäßigen "typüberprüfbaren Verweis auf irgendetwas" hat. Dies löst das oben erwähnte Problem nicht, wenn der Sprachstandard nicht vorhanden ist, dh wenn die Implementierungen verschiedener Programmierer nicht kompatibel sind. Es erklärt jedoch, wie C ++ auskommt, ohne einen Basistyp zu haben, von dem alles abgeleitet ist: indem es erstellt werden kann etwas, das sich wie eins verhält.quelle
Woofer
es sich um eine SchnittstelleCat
handelt, die vererbbar ist, wäre die Besetzung legitim, da es (wenn nicht jetzt, möglicherweise in Zukunft) eine geben könnte, vonWoofingCat
der erbtCat
und die implementiert wirdWoofer
. Beachten Sie, dass unter dem Java Kompilierung / Verknüpfung Modell, Schaffung einesWoofingCat
keinen Zugriff auf den Quellcode für erfordern würdeCat
nochWoofer
.Cat
nach A ordnungsgemäß handhabtWoofer
und die Frage beantwortet, ob Sie in X konvertierbar sind. Mit C ++ können Sie eine Besetzung erzwingen, denn hey, vielleicht wissen Sie tatsächlich, was Sie tun, aber es hilft Ihnen auch, wenn Sie das nicht wirklich vorhaben.dynamic_cast
hat ein definiertes Verhalten, wenn er auf ein polymorphes Objekt zeigt, und undefiniertes Verhalten, wenn nicht, aus semantischer Sicht ...Symbian C ++ hatte in der Tat eine universelle Basisklasse, CBase, für alle Objekte, die sich auf eine bestimmte Art und Weise verhielten (hauptsächlich, wenn sie Heap reservierten). Es stellte einen virtuellen Destruktor bereit, setzte den Speicher der Klasse beim Konstruieren auf Null und versteckte den Kopierkonstruktor.
Das Grundprinzip dahinter war, dass es eine Sprache für eingebettete Systeme war und C ++ - Compiler und Spezifikationen vor 10 Jahren wirklich wirklich scheiße waren.
Nicht alle Klassen haben davon geerbt, nur einige.
quelle