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

76

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?

Matthew James Briggs
quelle
16
weil C ++ kein Java ist ... Und Sie sollten es nicht erzwingen.
AK_
10
Frage zu Stack Overflow: Warum gibt es in C ++ keine Basisklasse?
26
Ich bin auch nicht einverstanden mit den engen Abstimmungen für "hauptsächlich meinungsbasiert". Es gibt sehr spezifische Gründe, die dafür erklärt werden können, da die Antworten sowohl auf diese Frage als auch auf die verknüpfte SO-Frage bezeugen.
2
Es ist das "Du wirst es nicht brauchen" Prinzip der Agilität. Tun Sie dies erst, wenn Sie einen bestimmten Bedarf bereits erkannt haben.
Jool
3
@AK_: In deinem Kommentar fehlt ein "so dumm wie".
DeadMG

Antworten:

75

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 stattdessen std::vectorungeordnete 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- QObjectKlasse, 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).

Ratschenfreak
quelle
7
Sie haben wahrscheinlich vergessen, den Hauptgrund für die Verwendung einer Basisklasse in Java zu nennen: Vor Generics benötigten die Auflistungsklassen die Basisklasse, um zu funktionieren. Alles (interner Speicher, Parameter, Rückgabewerte) wurde eingegeben Object.
Aleksandr Dubinsky
1
@AleksandrDubinsky: Und Generika fügten nur syntaktischen Zucker hinzu und änderten nichts wirklich als den Glanz.
Deduplizierer
4
Ich würde behaupten, Hash-Code, Gleichheit und Monitorunterstützung sind auch in Java Designfehler. Wer hielt es für eine gute Idee, alle Objekte zu sperren ?!
USR
1
Ja, aber niemand will es. Wenn das letzte Mal war , man benötigt ein Objekt zu sperren und konnte nicht über ein separates Sperrobjekt zu tun , dass instanziiert. Es ist sehr selten und belastet alles. Die Java-Leute hatten damals ein schlechtes Verständnis von Thread-Sicherheit, was beweist, dass alle Objekte eine Sperre sind und dass die thread-sicheren Sammlungen mittlerweile veraltet sind. Thread-Sicherheit ist eine globale Eigenschaft, keine objektspezifische.
USR
2
HashCode nur dann sinnvoll ist , wenn Sie es in einer Hash-basierten Sammlung speichern wollen (die Präferenz in C ++ ist für std :: vector und schlicht ungeordnete Listen). “ Das wirkliche Gegenargument _hashCodeist nicht ‚einen anderen Behälter verwenden‘ , sondern zeigt darauf hin, dass C ++ std::unordered_mapHashing 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.
Underscore_d
100

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.

DeadMG
quelle
10
+1 zur Vereinfachung der Antwort, das ist wirklich der einzige Grund.
BWG
7
In dem umfangreichen Framework, mit dem ich Erfahrung habe, stellt die gemeinsame Basisklasse die Serialisierungs- und Reflektionsinfrastruktur bereit, die im <whatever> -Kontext gewünscht wird. Meh. Es führte nur dazu, dass Leute eine Menge Cruft zusammen mit den Daten und Metadaten serialisierten und das Datenformat zu umfangreich und komplex machte, um effizient zu sein.
dmckee
19
@dmckee: Ich würde auch argumentieren, dass Serialisierung und Reflektion kaum universell brauchbare Bedürfnisse sind.
DeadMG
16
@DeadMG: "ABER WAS MÜSSEN SIE ALLES SPEICHERN?"
Deworde
8
Ich weiß nicht, du schreibst es in Anführungszeichen, du verwendest alle Kappen und die Leute können den Witz nicht sehen. @MSalters: Nun, das sollte einfach sein, es hat eine minimale Menge an Status, Sie geben nur an, dass es da ist. Ich kann meinen Namen in eine Liste schreiben, ohne eine rekursive Schleife einzugeben.
Deworde
25

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.

Mike Nakis
quelle
6
Wenn die Basisklasse (in Java "java.lang.Object") keine Methoden enthält, die andere Methoden aufrufen, kann das Problem "Fragile Base Class" nicht auftreten.
Martin Rosenau
3
Eine mächtige nützliche Basisklasse, die wäre!
Mike Nakis
9
@MartinRosenau ... wie Sie es in C ++ tun können, ohne eine Master-Basisklasse zu benötigen!
gbjbaanb
5
@ DavorŽdralo C ++ hat also einen dummen Namen für eine Grundfunktion ("operator <<" anstelle von etwas Sinnvollem wie "DebugPrint"), während Java für absolut jede Klasse, die Sie schreiben, einen Freak einer Basisklasse hat, keine Ausnahmen. Ich denke, ich mag C ++ 's Warze mehr.
Sebastian Redl
4
@ DavorŽdralo: Der Name der Funktion ist irrelevant. Bild eine Syntax cout.print(x).print(0.5).print("Bye\n")- es hängt nicht davon ab operator<<.
MSalters
24

Weil:

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

Die Implementierung jeder Art virtualstellt eine virtuelle Funktion-Tabelle, die Raum - Overhead pro-Objekt erfordert , die weder notwendig noch erwünscht ist in vielen ( die meisten?) Situationen.

Eine nicht toStringvirtuelle 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 equalsoder hashCodenur 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 eine intsollte wirklich keine andere Identität als ihren Wert haben ... zwei ganze Zahlen desselben Werts sollten gleich sein.)

Mehrdad
quelle
Beachten Sie in Bezug auf dieses Problem und das von Mike Nakis festgestellte fragile Basisklassenproblem die interessante Untersuchung / den Vorschlag , es in Java zu beheben, indem Sie alle Methoden intern (dh wenn sie von derselben Klasse aufgerufen werden) nicht-virtuell machen, aber ihr virtuelles Verhalten beibehalten, wenn extern angerufen; Um altes / standardmäßiges Verhalten (dh virtuell überall) zu erhalten, wurde in dem Vorschlag ein neues openSchlüsselwort eingeführt. Ich glaube nicht, dass es über ein paar Papiere hinausging.
Fizz
Weitere Informationen
Fizz
Eine gemeinsame Basisklasse Having würde es möglich machen , zu testen , jeden shared_ptr<Foo> zu sehen , ob es auch ein ist shared_ptr<Bar>(oder auch mit anderen Zeigertypen), auch wenn Foound Barnicht 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.
Supercat
Es mag nicht hilfreich sein, für alles eine gemeinsame Basisklasse zu haben, aber ich denke, es gibt einige ziemlich große Kategorien von Objekten, für die gemeinsame Basisklassen hilfreich wären. Beispielsweise können viele Klassen (eine beträchtliche Anzahl, wenn nicht die Mehrheit) in Java auf zwei Arten verwendet werden: als nicht gemeinsam genutzter Inhaber veränderlicher Daten oder als gemeinsam nutzbarer Inhaber von Daten, die niemand ändern darf. Bei beiden Verwendungsmustern wird ein verwalteter Zeiger (Referenz) als Proxy für die zugrunde liegenden Daten verwendet. Es ist hilfreich, einen gemeinsamen verwalteten Zeigertyp für alle diese Daten zu haben.
Supercat
16

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_castWenn Sie Container von allem benötigen, boost::anykönnen Sie dies jedoch auch ohne eine gemeinsame Root-Klasse tun . Und boost::anyunterstü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:

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 Bit Code, das Sie schreiben, sein serialization_friendly.

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

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 ein tupleoder ein intoder ein doubleoder 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 erweitern, write_toindem wir write_toals 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_serializeSchnittstelle packen, mit der Sie serializezu einem späteren Zeitpunkt über eine virtuelle Schnittstelle zugreifen können.

Damit:

void writer_thingy( can_serialize s );

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

void writer_thingy( serialization_friendly const* s );

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_serializeund die Serialisierung der std::vectorwird 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.

Yakk
quelle
8

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.

Jules
quelle
1
Eine Lösung für das Diamantproblem besteht darin, dass alle Typen, die einen Basistyp nicht virtuell über mehrere Pfade ableiten, alle virtuellen Mitglieder dieses Basistyps überschreiben. Wenn von Anfang an ein gemeinsamer Basistyp in die Sprache integriert worden wäre, könnte ein Compiler automatisch legitime (wenn auch nicht unbedingt beeindruckende) Standardimplementierungen generieren.
Supercat
5

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 der std::vector<CObject>heutigen vergleichbar ist. Da CObjectes sich bei fast allen Klassen um die Basisklasse handelte (leider nicht von CString- das Äquivalent zu stringmodernen 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.

Martin Rosenau
quelle
3
Ist das MFC? [Kommentarauffüllung]
user253751
3
Es ist in der Tat MFC. Ein leuchtendes Leuchtfeuer im OO-Design, das der Welt zeigte, wie etwas zu tun ist. Oh, warte ...
gbjbaanb
4
@gbjbaanb Turbo Pascal und Turbo C ++ hatten ihre eigenen, TObjectbevor 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.
HDV
Sogar vor Vorlagen führte der Versuch, Smalltalk in C ++ zu schreiben, zu schrecklichen Ergebnissen.
JDługosz
@hvd Dennoch war MFC ein viel schlechteres Beispiel für objektorientiertes Design als alles, was Borland produzierte.
Jules
5

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::vectorhier nicht verwenden , da es nicht implementiert wird IObject- ich sollte ein neues abgeleitetes erstellen IVectorObject, 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 Integerund die intVerwirrung 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.)

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();
  }
}
Michael Anderson
quelle
3

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önnen static_castzur ursprünglichen Klasse zurückkehren.

Doch das, was Sie nicht können mit tun , voiddie Sie tun können Objectist 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.

pjc50
quelle
1
Nur Basisklasse-Unterobjekte mit null Breite, keine normalen.
Deduplizierer
@Deduplicator Als Update fügt C ++ 17 hinzu [[no_unique_address]], das von Compilern verwendet werden kann, um Member-Unterobjekten die Breite Null zuzuweisen.
Underscore_d
1
@underscore_d Du meinst für C ++ 20 geplant, [[no_unique_address]]erlaubt dem Compiler EBO Member-Variablen.
Deduplicator
@ Deduplicator Hoppla, ja. Ich habe bereits angefangen, C ++ 17 zu verwenden, aber ich denke immer noch, dass es aktueller ist als es wirklich ist!
Underscore_d
2

Java geht von der Designphilosophie aus, dass Undefiniertes Verhalten nicht existieren sollte . Code wie:

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

testet, ob felixein Subtyp Catdieses Interfaces vorhanden ist Woofer; 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 er feliximplementiert ist Wooferoder 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 *Catin 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.

Superkatze
quelle
Diese Umwandlung schlägt zur Kompilierungszeit in C ++ , Java und C # fehl .
Milleniumbug
1
@milleniumbug: Wenn Wooferes sich um eine Schnittstelle Cathandelt, die vererbbar ist, wäre die Besetzung legitim, da es (wenn nicht jetzt, möglicherweise in Zukunft) eine geben könnte, von WoofingCatder erbt Catund die implementiert wird Woofer. Beachten Sie, dass unter dem Java Kompilierung / Verknüpfung Modell, Schaffung eines WoofingCatkeinen Zugriff auf den Quellcode für erfordern würde Catnoch Woofer.
Supercat
3
C ++ verfügt über dynamic_cast , das das Umwandeln von A Catnach A ordnungsgemäß handhabt Wooferund 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.
Rob K
2
@RobK: Sie haben natürlich Recht mit der Syntax. Mea culpa. Ich habe ein bisschen mehr über dynamic_cast gelesen und es scheint, dass in einem gewissen Sinne in modernem C ++ alle polymorphen Objekte von einer Basisklasse für "polymorphe Objekte" abgeleitet wurden, mit allen Feldern, die zur Identifizierung des Objekttyps erforderlich sind (normalerweise eine vtable) Zeiger, obwohl das ein Implementierungsdetail ist). C ++ beschreibt polymorphe Klassen nicht auf diese Weise, aber die Übergabe eines Zeigers auf dynamic_casthat ein definiertes Verhalten, wenn er auf ein polymorphes Objekt zeigt, und undefiniertes Verhalten, wenn nicht, aus semantischer Sicht ...
supercat
2
... alle polymorphen Objekte einige Informationen mit demselben Layout speichern und alle ein Verhalten unterstützen, das von nicht polymorphen Objekten nicht unterstützt wird; Für mich bedeutet dies, dass sie sich so verhalten, als würden sie von einer gemeinsamen Basis abgeleitet, unabhängig davon, ob die Sprachdefinition eine solche Terminologie verwendet oder nicht.
Supercat
1

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.

James
quelle