it-swarm.com.de

Warum wird von der Basis für alle Objekte in C ++ abgeraten?

Stroustrup sagt: "Erfinde nicht sofort eine eindeutige Basis für alle deine Klassen (eine Objektklasse). Normalerweise kannst du 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?

77

Denn was hätte dieses Objekt für die Funktionalität? In Java hat die Basisklasse lediglich einen toString, einen HashCode & Equality und eine Monitor + Bedingungsvariable.

  • ToString ist nur zum Debuggen nützlich.

  • hashCode ist nur nützlich, wenn Sie es in einer Hash-basierten Auflistung speichern möchten (in C++ wird bevorzugt, eine Hash-Funktion als Vorlagenparameter an den Container zu übergeben oder std::unordered_* insgesamt zu vermeiden und stattdessen std::vector und einfache ungeordnete Listen).

  • gleichheit ohne Basisobjekt kann beim Kompilieren unterstützt werden. Wenn sie nicht denselben Typ haben, können sie nicht gleich sein. In C++ ist dies ein Fehler bei der Kompilierung.

  • die Monitor- und Bedingungsvariable wird von Fall zu Fall besser explizit einbezogen.

Wenn jedoch mehr getan werden muss, gibt es einen Anwendungsfall.

Zum Beispiel gibt es in QT die Root-Klasse QObject, die die Grundlage für die Thread-Affinität, die Eltern-Kind-Besitzhierarchie und den Signalschlitzmechanismus bildet. Es erzwingt auch die Verwendung durch Zeiger für QObjects, jedoch viele Klassen in Qt erben QObject nicht, da sie den Signalschlitz nicht benötigen (insbesondere die Werttypen einiger Beschreibungen).

74
ratchet freak

Weil es keine Funktionen gibt, die von allen Objekten gemeinsam genutzt werden. In dieser Schnittstelle gibt es nichts, was für alle Klassen sinnvoll wäre.

100
DeadMG

Immer wenn Sie große Vererbungshierarchien von Objekten erstellen, stoßen Sie auf das Problem der Fragile Base Class (Wikipedia.) .

Viele kleine separate (unterschiedliche, isolierte) Vererbungshierarchien verringern die Wahrscheinlichkeit, auf dieses Problem zu stoßen.

Wenn Sie alle Ihre Objekte zu einer einzigen riesigen Vererbungshierarchie machen, wird praktisch sichergestellt, dass Sie auf dieses Problem stoßen.

25
Mike Nakis

Weil:

  1. Sie sollten nicht für das bezahlen, was Sie nicht verwenden.
  2. Diese Funktionen sind in einem wertbasierten Typsystem weniger sinnvoll als in einem referenzbasierten Typsystem.

Durch die Implementierung der Funktion any sort virtual wird eine virtuelle Tabelle eingeführt, für die pro Objekt Speicherplatz erforderlich ist, der in vielen (den meisten?) Situationen weder erforderlich noch erwünscht ist.

Das nicht-virtuelle Implementieren von toString wäre ziemlich nutzlos, da das einzige, was zurückgegeben werden könnte, die Adresse des Objekts ist, die sehr benutzerunfreundlich ist und auf die der Aufrufer im Gegensatz zu Java bereits Zugriff hat.
Ebenso könnte ein nicht virtuelles equals oder hashCode nur Adressen zum Vergleichen von Objekten verwenden, was wiederum ziemlich nutzlos und oft sogar völlig falsch ist - im Gegensatz zu Java werden Objekte häufig kopiert in C++, und daher ist die Unterscheidung der "Identität" eines Objekts nicht immer sinnvoll oder nützlich. (z. B. sollte ein int wirklich nicht eine andere Identität als seinen Wert haben ... zwei ganze Zahlen desselben Werts sollten gleich sein.)

24
user541686

Ein einziges Stammobjekt schränkt ein, was Sie tun können und was der Compiler tun kann, ohne viel Geld auszuzahlen.

Eine gemeinsame Root-Klasse ermöglicht es, Container mit allem zu erstellen und mit einem dynamic_cast Zu extrahieren, was sie sind. Wenn Sie jedoch Container mit allem benötigen, kann dies mit etwas ähnlichem wie boost::any Ausgeführt werden ohne eine gemeinsame Wurzelklasse. Und boost::any Unterstützt auch Grundelemente - es kann sogar die Optimierung kleiner Puffer unterstützen und sie in Java Sprache) fast "unboxed" lassen.

C++ unterstützt und lebt von Werttypen. Sowohl Literale als auch vom Programmierer geschriebene Werttypen. C++ - Container speichern, sortieren, hashen, konsumieren und produzieren Werttypen effizient.

Die Vererbung, insbesondere die Art der monolithischen Vererbung Java -Stil-Basisklassen implizieren, erfordert frei speicherbasierte "Zeiger" - oder "Referenz" -Typen. Ihr Handle/Zeiger/Verweis auf Daten enthält einen Zeiger auf die Schnittstelle der Klasse und könnte polymorph etwas anderes darstellen.

Während dies in einigen Situationen nützlich ist, haben Sie, nachdem Sie sich mit dem Muster mit einer "gemeinsamen Basisklasse" verheiratet haben, 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" entweder am aufrufenden Standort oder in dem Code, der ihn verwendet.

Wenn die Funktion einfach ist, erhalten Sie durch das Schreiben der Funktion als Vorlage einen kompilierzeitbasierten Polymorphismus vom Ententyp, bei dem Informationen an der aufrufenden Site nicht weggeworfen werden. Wenn die Funktion komplexer ist, kann eine Typlöschung durchgeführt werden, bei der die einheitlichen Operationen für den Typ, den Sie ausführen möchten (z. B. Serialisierung und Deserialisierung), erstellt und gespeichert werden (zur Kompilierungszeit), um von der verwendet zu werden (zur Laufzeit) Code in einer anderen Übersetzungseinheit.

Angenommen, Sie haben eine Bibliothek, in der alles serialisierbar sein soll. Ein Ansatz besteht darin, eine Basisklasse zu haben:

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

Jetzt kann jedes Codebit, das Sie schreiben, serialization_friendly Sein.

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

Außer kein std::vector, Also müssen Sie jetzt jeden Container schreiben. Und nicht die ganzen Zahlen, die Sie aus dieser Bignum-Bibliothek erhalten haben. Und nicht der Typ, den Sie geschrieben haben, von dem Sie nicht dachten, dass er eine Serialisierung benötigt. Und nicht ein Tuple oder ein int oder ein double oder ein std::ptrdiff_t.

Wir verfolgen einen anderen Ansatz:

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

was darin besteht, scheinbar nichts zu tun. Außer jetzt können wir write_to Erweitern, indem wir write_to Als freie Funktion im Namespace eines Typs oder einer Methode im Typ überschreiben.

Wir können sogar ein bisschen Löschcode schreiben:

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

und jetzt können wir einen beliebigen Typ nehmen und ihn automatisch in eine can_serialize - Schnittstelle einbinden, mit der Sie serialize zu einem späteren Zeitpunkt über eine virtuelle Schnittstelle aufrufen können.

Damit:

void writer_thingy( can_serialize s );

ist eine Funktion, die alles verwendet, was serialisiert werden kann, anstatt

void writer_thingy( serialization_friendly const* s );

und das erste kann im Gegensatz zum zweiten int, std::vector<std::vector<Bob>> automatisch verarbeiten.

Das Schreiben hat nicht viel gekostet, vor allem, weil Sie so etwas nur selten tun möchten, aber wir haben die Möglichkeit erhalten, alles als serialisierbar zu behandeln, ohne einen Basistyp zu benötigen.

Darüber hinaus können wir std::vector<T> Jetzt als erstklassigen Bürger serialisierbar machen, indem wir einfach write_to( my_buffer*, std::vector<T> const& ) überschreiben - mit dieser Überladung kann es an einen can_serialize Und den übergeben werden Die Serialisierbarkeit von std::vector wird in einer vtable gespeichert und von .write_to aufgerufen.

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 Basis (gefälscht oder nicht) benötigt wird, sind ziemlich 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 Cache-Freundlichkeit auf modernen Prozessoren sehr wichtig ist). Compiler können leicht verstehen, was eine bestimmte Operation tut (anstatt einen undurchsichtigen virtuellen Methodenzeiger zu haben, über den sie springen muss, führt dies zu unbekanntem Code auf dem andere Seite), wodurch Anweisungen optimal neu angeordnet werden können und weniger runde Stifte in runde Löcher gehämmert werden.

16
Yakk

Es gibt oben viele gute Antworten, und die klare 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: Vermeiden Sie das Erstellen von Vererbungsdiamanten , wenn 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 Sie zugreifen möchten, da diese in verschiedenen Pfaden der Vererbungskette unterschiedlich überladen werden kann. Und 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.

8
Jules

Tatsächlich hatten die frühen C++ - Compiler und -Bibliotheken von Microsoft (ich kenne Visual C++, 16 Bit) eine solche Klasse mit dem Namen CObject.

Sie müssen jedoch wissen, dass "Vorlagen" zu diesem Zeitpunkt von diesem einfachen C++ - Compiler nicht unterstützt wurden, sodass Klassen wie std::vector<class T> Nicht möglich waren. Stattdessen konnte eine "Vektor" -Implementierung nur einen Klassentyp verarbeiten, sodass es heute eine Klasse gab, die mit std::vector<CObject> Vergleichbar ist. Da CObject die Basisklasse fast aller Klassen war (leider nicht von CString - das Äquivalent von string in modernen Compilern), können Sie diese Klasse zum Speichern fast aller Arten von verwenden Objekte.

Da moderne Compiler Vorlagen unterstützen, wird dieser Anwendungsfall einer "generischen Basisklasse" nicht mehr angegeben.

Sie müssen darüber nachdenken, dass die Verwendung einer solchen generischen Basisklasse (ein wenig) 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 fast keinen Anwendungsfall für eine solche Klasse.

5
Martin Rosenau

Ich werde einen anderen Grund vorschlagen, der von Java kommt.

Weil d keine Basisklasse für alles erstellen kann, zumindest nicht ohne ein paar Kesselplatten.

Möglicherweise können Sie damit für Ihre eigenen Klassen durchkommen - aber Sie werden wahrscheinlich feststellen, dass Sie am Ende viel Code duplizieren. Z.B. "Ich kann std::vector Hier nicht verwenden, da es IObject nicht implementiert - ich sollte besser ein neues abgeleitetes IVectorObject erstellen, 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 integriert wäre, würden Sie Dinge wie die Verwirrung Integer und int in Java oder eine große Änderung der Sprachsyntax haben. (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 zur Laufzeit polymorph ist (d. H. Virtuelle Funktionen verwendet).

z.B. Anstelle von .toString() könnten Sie Folgendes haben: (HINWEIS: Ich weiß, dass Sie dies ordentlicher mit vorhandenen Bibliotheken usw. tun können. Dies ist nur ein veranschaulichendes Beispiel.)

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();
  }
}
5

Wohl "void" erfüllt viele der Rollen einer universellen Basisklasse. Sie können einen beliebigen Zeiger auf ein void*. Sie können diese Zeiger dann vergleichen. Du kannst static_cast zurück zur ursprünglichen Klasse.

Was Sie jedoch nicht mit void tun können, was Sie mit Object tun können, ist RTTI zu verwenden, um herauszufinden, welchen Objekttyp Sie wirklich haben. Dies hängt letztendlich damit zusammen, dass nicht alle Objekte in C++ RTTI haben, und es ist tatsächlich möglich, Objekte mit einer Breite von Null zu haben.

3
pjc50

Java nimmt die Designphilosophie an, dass ndefiniertes Verhalten sollte nicht existieren. Code wie:

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

testet, ob felix einen Subtyp von Cat enthält, der die Schnittstelle Woofer implementiert; Wenn dies der Fall ist, führt es die Umwandlung durch und ruft woof() auf. Wenn dies nicht der Fall ist, wird eine Ausnahme ausgelöst. Das Verhalten des Codes ist vollständig definiert, ob felixWoofer implementiert oder nicht.

C++ vertritt die Philosophie, dass es keine Rolle spielen sollte, was ein generierter Code tun würde, wenn diese Operation ausgeführt würde, wenn ein Programm eine Operation nicht versuchen sollte, und dass der Computer keine Zeit damit verschwenden sollte, das Verhalten in Fällen einzuschränken, die "sollten". niemals entstehen. Wenn Sie in C++ die entsprechenden Indirektionsoperatoren hinzufügen, um einen *Cat In einen *Woofer Umzuwandeln, würde der Code ein definiertes Verhalten ergeben, wenn die Umwandlung legitim ist, aber undefiniertes Verhalten, wenn dies der Fall ist nicht.

Ein gemeinsamer Basistyp für Dinge ermöglicht es, Casts unter Derivaten dieses Basistyps zu validieren und auch Try-Cast-Operationen durchzuführen. Das Validieren von Casts ist jedoch teurer als die bloße Annahme, dass sie legitim sind und zu hoffen, dass nichts Schlimmes passiert. Die C++ - Philosophie wäre, dass für eine solche Validierung "für etwas bezahlt werden muss, das Sie [normalerweise] nicht benötigen".

Ein weiteres Problem, das sich auf C++ bezieht, für eine neue Sprache jedoch kein Problem darstellt, besteht darin, dass, wenn mehrere Programmierer jeweils eine gemeinsame Basis erstellen, daraus ihre eigenen Klassen abgeleitet werden und Code geschrieben wird, um mit Dingen dieser gemeinsamen Basisklasse zu arbeiten. Ein solcher Code kann nicht mit Objekten arbeiten, die von Programmierern entwickelt wurden, die eine andere Basisklasse verwendet haben. Wenn eine neue Sprache erfordert, dass alle Heap-Objekte ein gemeinsames Header-Format haben und niemals Heap-Objekte zugelassen haben, 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 Objekt häufig zu fragen, ob Sie in Typ X konvertierbar sind. Wenn ein solches Merkmal jedoch nicht von Anfang an in eine Sprache integriert ist, ist dies schwierig füge es später hinzu. Persönlich denke ich, dass eine solche Basisklasse bei der ersten Gelegenheit zu einer Standardbibliothek hinzugefügt werden sollte, mit der starken Empfehlung, dass alle Objekte, die polymorph verwendet werden, von dieser Basis erben sollten. Wenn Programmierer jeweils ihre eigenen "Basistypen" implementieren, würde das Übergeben von Objekten zwischen dem Code verschiedener Personen schwieriger, aber ein gemeinsamer Basistyp, von dem viele Programmierer geerbt haben, würde es einfacher machen.

NACHTRAG

Mithilfe von Vorlagen können Sie einen "beliebigen Objekthalter" definieren und ihn nach dem Typ des darin enthaltenen Objekts fragen. Das Boost-Paket enthält so etwas wie any. Obwohl C++ keinen Standardtyp "typprüfbare Referenz auf irgendetwas" hat, ist es daher möglich, einen zu erstellen. Dies löst nicht das oben erwähnte Problem, dass es nichts im Sprachstandard gibt, dh Inkompatibilität zwischen den Implementierungen verschiedener Programmierer, aber es erklärt, wie C++ ohne einen Basistyp auskommt, von dem alles abgeleitet ist: indem es das Erstellen ermöglicht etwas, das sich wie eins verhält.

2
supercat

Symbian C++ hatte tatsächlich eine universelle Basisklasse, CBase, für alle Objekte, die sich auf eine bestimmte Weise verhielten (hauptsächlich, wenn sie Heap zuweisen würden). Es stellte einen virtuellen Destruktor bereit, stellte den Speicher der Klasse beim Erstellen 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.

1
James