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 . Und boost::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:
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 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 erweitern, write_to
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 packen, mit der Sie serialize
zu 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_serialize
und die Serialisierung der std::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.