Fast jede C ++ - Ressource, die ich gesehen habe und die solche Dinge behandelt, sagt mir, dass ich polymorphe Ansätze der Verwendung von RTTI (Runtime Type Identification) vorziehen sollte. Im Allgemeinen nehme ich diese Art von Rat ernst und werde versuchen, die Gründe zu verstehen - schließlich ist C ++ ein mächtiges Tier und in seiner ganzen Tiefe schwer zu verstehen. Für diese spezielle Frage zeichne ich jedoch eine Lücke und möchte sehen, welche Art von Rat das Internet bieten kann. Lassen Sie mich zunächst zusammenfassen, was ich bisher gelernt habe, indem ich die häufigsten Gründe aufführe, aus denen RTTI als "schädlich" eingestuft wird:
Einige Compiler verwenden es nicht / RTTI ist nicht immer aktiviert
Ich kaufe dieses Argument wirklich nicht. Es ist so, als würde ich sagen, ich sollte keine C ++ 14-Funktionen verwenden, da es Compiler gibt, die dies nicht unterstützen. Und doch würde mich niemand davon abhalten, C ++ 14-Funktionen zu verwenden. Die meisten Projekte haben Einfluss auf den verwendeten Compiler und dessen Konfiguration. Sogar die gcc-Manpage zitieren:
-fno-rtti
Deaktivieren Sie die Generierung von Informationen zu jeder Klasse mit virtuellen Funktionen zur Verwendung durch die C ++ - Laufzeit-Typidentifizierungsfunktionen (dynamic_cast und typeid). Wenn Sie diese Teile der Sprache nicht verwenden, können Sie mit diesem Flag Platz sparen. Beachten Sie, dass die Ausnahmebehandlung dieselben Informationen verwendet, diese jedoch von G ++ nach Bedarf generiert werden. Der Operator dynamic_cast kann weiterhin für Casts verwendet werden, für die keine Laufzeitinformationen erforderlich sind, dh für Casts in "void *" oder in eindeutige Basisklassen.
Dies sagt mir, dass ich es deaktivieren kann , wenn ich RTTI nicht verwende. Das heißt, wenn Sie Boost nicht verwenden, müssen Sie keine Verknüpfung herstellen. Ich muss nicht für den Fall planen, mit dem jemand kompiliert -fno-rtti
. Außerdem fällt der Compiler in diesem Fall laut und deutlich aus.
Es kostet zusätzlichen Speicher / Kann langsam sein
Wenn ich versucht bin, RTTI zu verwenden, bedeutet dies, dass ich auf eine Art von Typinformation oder Merkmal meiner Klasse zugreifen muss. Wenn ich eine Lösung implementiere, die kein RTTI verwendet, bedeutet dies normalerweise, dass ich meinen Klassen einige Felder hinzufügen muss, um diese Informationen zu speichern, sodass das Speicherargument irgendwie ungültig ist (ich werde weiter unten ein Beispiel dafür geben).
Ein dynamic_cast kann tatsächlich langsam sein. Es gibt jedoch normalerweise Möglichkeiten, um zu vermeiden, dass geschwindigkeitskritische Situationen verwendet werden müssen. Und ich sehe die Alternative nicht ganz. Diese SO-Antwort schlägt vor, eine in der Basisklasse definierte Aufzählung zum Speichern des Typs zu verwenden. Das funktioniert nur, wenn Sie alle abgeleiteten Klassen a priori kennen. Das ist ein ziemlich großes "Wenn"!
Aus dieser Antwort geht auch hervor, dass die Kosten für RTTI ebenfalls nicht klar sind. Unterschiedliche Menschen messen unterschiedliche Dinge.
Elegante polymorphe Designs machen RTTI unnötig
Dies ist die Art von Rat, die ich ernst nehme. In diesem Fall kann ich einfach keine guten Nicht-RTTI-Lösungen finden, die meinen RTTI-Anwendungsfall abdecken. Lassen Sie mich ein Beispiel geben:
Angenommen, ich schreibe eine Bibliothek, um Diagramme von Objekten zu verarbeiten. Ich möchte Benutzern erlauben, ihre eigenen Typen zu generieren, wenn sie meine Bibliothek verwenden (daher ist die Aufzählungsmethode nicht verfügbar). Ich habe eine Basisklasse für meinen Knoten:
class node_base
{
public:
node_base();
virtual ~node_base();
std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};
Jetzt können meine Knoten von verschiedenen Typen sein. Wie wäre es mit diesen:
class red_node : virtual public node_base
{
public:
red_node();
virtual ~red_node();
void get_redness();
};
class yellow_node : virtual public node_base
{
public:
yellow_node();
virtual ~yellow_node();
void set_yellowness(int);
};
Verdammt, warum nicht auch nur eines davon:
class orange_node : public red_node, public yellow_node
{
public:
orange_node();
virtual ~orange_node();
void poke();
void poke_adjacent_oranges();
};
Die letzte Funktion ist interessant. Hier ist eine Möglichkeit, es zu schreiben:
void orange_node::poke_adjacent_oranges()
{
auto adj_nodes = get_adjacent_nodes();
foreach(auto node, adj_nodes) {
// In this case, typeid() and static_cast might be faster
std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
if (o_node) {
o_node->poke();
}
}
}
Das alles scheint klar und sauber. Ich muss keine Attribute oder Methoden definieren, bei denen ich sie nicht benötige. Die Basisknotenklasse kann schlank und gemein bleiben. Wo fange ich ohne RTTI an? Vielleicht kann ich der Basisklasse ein node_type-Attribut hinzufügen:
class node_base
{
public:
node_base();
virtual ~node_base();
std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
private:
std::string my_type;
};
Ist std :: string eine gute Idee für einen Typ? Vielleicht nicht, aber was kann ich noch verwenden? Erstelle eine Nummer und hoffe, dass sie noch niemand benutzt? Was ist bei meinem orange_node, wenn ich die Methoden von red_node und yellow_node verwenden möchte? Müsste ich mehrere Typen pro Knoten speichern? Das scheint kompliziert.
Fazit
Dieses Beispiel scheint nicht allzu komplex oder ungewöhnlich zu sein (ich arbeite an etwas Ähnlichem in meinem Tagesjob, bei dem die Knoten die tatsächliche Hardware darstellen, die über die Software gesteuert wird und die je nach dem, was sie sind, sehr unterschiedliche Aufgaben ausführen). Ich würde jedoch keinen sauberen Weg finden, dies mit Vorlagen oder anderen Methoden zu tun. Bitte beachten Sie, dass ich versuche, das Problem zu verstehen und mein Beispiel nicht zu verteidigen. Das Lesen von Seiten wie der SO-Antwort, die ich oben verlinkt habe, und dieser Seite in Wikibooks scheinen darauf hinzudeuten, dass ich RTTI missbrauche, aber ich würde gerne erfahren, warum.
Zurück zu meiner ursprünglichen Frage: Warum ist "reiner Polymorphismus" der Verwendung von RTTI vorzuziehen?
quelle
node_base
stelle fest, dass viele Antworten nicht die Idee berücksichtigen, dass Ihr Beispiel vorschlägt, Teil einer Bibliothek zu sein, und dass Benutzer ihre eigenen Knotentypen erstellen. Dann können sie keine Änderungen vornehmennode_base
, um eine andere Lösung zuzulassen. Vielleicht ist RTTI dann die beste Option. Auf der anderen Seite gibt es andere Möglichkeiten, eine solche Bibliothek so zu gestalten, dass neue Knotentypen viel eleganter passen, ohne dass RTTI verwendet werden muss (und auch andere Möglichkeiten, die neuen Knotentypen zu entwerfen).Antworten:
Eine Schnittstelle beschreibt, was man wissen muss, um in einer bestimmten Situation im Code zu interagieren. Sobald Sie die Schnittstelle mit „Ihrer gesamten Typenhierarchie“, Ihre Schnittstelle „Oberfläche“ wird riesig erweitern, die darüber macht Argumentation härter .
Zum Beispiel bedeutet Ihr "Anstoßen benachbarter Orangen", dass ich als Dritter nicht emulieren kann, eine Orange zu sein! Sie haben privat einen orangefarbenen Typ deklariert und dann RTTI verwendet, damit sich Ihr Code bei der Interaktion mit diesem Typ besonders verhält. Wenn ich "orange sein" will, muss ich in deinem privaten Garten sein.
Jetzt koppelt jeder, der mit "Orangenität" koppelt, mit Ihrem gesamten Orangentyp und implizit mit Ihrem gesamten privaten Garten, anstatt mit einer definierten Schnittstelle.
Auf den ersten Blick scheint dies eine großartige Möglichkeit zu sein, die eingeschränkte Benutzeroberfläche zu erweitern, ohne alle Clients ändern zu müssen (Hinzufügen
am_I_orange
). Stattdessen verknöchert dies die Codebasis und verhindert eine weitere Erweiterung. Die besondere Orangenität wird der Funktionsweise des Systems inhärent und verhindert, dass Sie einen "Mandarinen" -Ersatz für Orange erstellen, der anders implementiert ist und möglicherweise eine Abhängigkeit beseitigt oder ein anderes Problem elegant löst.Dies bedeutet, dass Ihre Schnittstelle ausreichen muss, um Ihr Problem zu lösen. Warum müssen Sie aus dieser Perspektive nur Orangen stupsen, und wenn ja, warum war die Orangenität in der Benutzeroberfläche nicht verfügbar? Wenn Sie einen unscharfen Satz von Tags benötigen, die ad-hoc hinzugefügt werden können, können Sie diesen Ihrem Typ hinzufügen:
Dies bietet eine ähnlich massive Erweiterung Ihrer Benutzeroberfläche von eng spezifiziert zu breit tagbasiert. Anstatt dies über RTTI und Implementierungsdetails zu tun (auch bekannt als "Wie werden Sie implementiert? Mit dem orangefarbenen Typ? Ok, Sie bestehen."), Wird dies mit etwas getan, das durch eine völlig andere Implementierung leicht emuliert werden kann.
Dies kann bei Bedarf sogar auf dynamische Methoden erweitert werden . "Unterstützen Sie es, mit den Argumenten Baz, Tom und Alice Foo'd zu sein? Ok, Fooing Sie." Im großen und ganzen ist dies weniger aufdringlich als eine dynamische Besetzung, um festzustellen, dass das andere Objekt ein Typ ist, den Sie kennen.
Jetzt können Mandarinenobjekte das orangefarbene Tag haben und mitspielen, während sie von der Implementierung entkoppelt werden.
Es kann immer noch zu einem großen Durcheinander führen, aber es ist zumindest ein Durcheinander von Nachrichten und Daten, keine Implementierungshierarchien.
Abstraktion ist ein Spiel zum Entkoppeln und Verbergen von Irrelevanzen. Es erleichtert das lokale Nachdenken über Code. RTTI bohrt ein Loch direkt durch die Abstraktion in Implementierungsdetails. Dies kann die Lösung eines Problems vereinfachen, hat jedoch die Kosten, Sie einfach an eine bestimmte Implementierung zu binden.
quelle
dynamic_cast
(RTTI). In diesem Fall sind die Tags redundant. Die zweite Möglichkeit, eine Gottesklasse, ist abscheulich. Zusammenfassend enthält diese Antwort viele Wörter, die meiner Meinung nach für Java-Programmierer gut klingen, aber der eigentliche Inhalt ist bedeutungslos.Die meisten der moralischen Appelle gegen diese oder jenes Merkmals sind Typizität aus der Beobachtung entstanden , dass es eine Umbra von ist misconceived Verwendungen dieser Funktion.
Wo Moralisten versagen, ist, dass sie davon ausgehen, dass ALLE Verwendungen falsch verstanden werden, während Merkmale tatsächlich aus einem bestimmten Grund existieren.
Sie haben , was ich verwendet , um die nennen „Klempner - Komplex“: sie denken alle Hähne sind Fehlfunktionen , da alle die Hähne sie zu reparieren genannt werden , sind. Die Realität ist, dass die meisten Wasserhähne gut funktionieren: Sie rufen einfach keinen Klempner für sie an!
Eine verrückte Sache, die passieren kann, ist, wenn Programmierer, um die Verwendung einer bestimmten Funktion zu vermeiden, viel Code schreiben, der genau diese Funktion privat neu implementiert. (Haben Sie jemals Klassen getroffen, die weder RTTI noch virtuelle Aufrufe verwenden, aber einen Wert haben, um zu verfolgen, um welchen tatsächlichen abgeleiteten Typ es sich handelt? Das ist nicht mehr als eine RTTI- Neuerfindung in Verkleidung.)
Es gibt eine allgemeine Art, über Polymorphismus nachzudenken :
IF(selection) CALL(something) WITH(parameters)
. (Entschuldigung, aber beim Programmieren geht es darum, wenn man die Abstraktion außer Acht lässt.)Die Verwendung von Entwurfszeit (Konzepte) Kompilierungszeit (Template-Deduction-basiert), Laufzeit (Vererbung und Virtual Function-basiert) oder datengesteuertem (RTTI und Switching) Polymorphismus hängt davon ab, wie viele Entscheidungen bekannt sind in jeder Phase der Produktion und wie variabel sie in jedem Kontext sind.
Die Idee ist, dass:
Je mehr Sie vorhersehen können, desto besser ist die Wahrscheinlichkeit, Fehler zu erkennen und Fehler zu vermeiden, die den Endbenutzer betreffen.
Wenn alles konstant ist (einschließlich der Daten), können Sie alles mit Vorlagen-Metaprogrammierung tun. Nachdem die Kompilierung für aktualisierte Konstanten erfolgt ist, läuft das gesamte Programm auf eine return-Anweisung hinaus, die das Ergebnis ausspuckt .
Wenn es eine Reihe von Fällen gibt, die alle zur Kompilierungszeit bekannt sind, Sie jedoch nicht wissen, auf welche Daten sie tatsächlich reagieren müssen, kann der Polymorphismus zur Kompilierungszeit (hauptsächlich CRTP oder ähnliches) eine Lösung sein.
Wenn die Auswahl der Fälle von den Daten abhängt (nicht bekannte Werte zur Kompilierungszeit) und die Umschaltung eindimensional ist (was zu tun ist, kann auf nur einen Wert reduziert werden), erfolgt der Versand auf der Basis virtueller Funktionen (oder allgemein "Funktionszeigertabellen" ") wird gebraucht.
Wenn die Umschaltung mehrdimensional ist, da in C ++ kein nativer Versand mit mehreren Laufzeiten vorhanden ist, müssen Sie entweder:
Wenn nicht nur die Umschaltung, sondern auch die Aktionen nicht zur Kompilierungszeit bekannt sind, ist Scripting & Parsing erforderlich: Die Daten selbst müssen die Aktion beschreiben, die auf sie ausgeführt werden soll.
Da jeder der von mir aufgezählten Fälle als ein besonderer Fall des Folgenden angesehen werden kann, können Sie jedes Problem lösen, indem Sie die unterste Lösung auch für Probleme missbrauchen, die mit den obersten erschwinglich sind.
Das ist es, was die Moralisierung tatsächlich zu vermeiden versucht . Das heißt aber nicht, dass es keine Probleme gibt, die in den untersten Domänen leben!
RTTI nur zu verprügeln, um es zu verprügeln, ist wie verprügeln,
goto
nur um es zu verprügeln. Dinge für Papageien, keine Programmierer.quelle
In einem kleinen Beispiel sieht es irgendwie ordentlich aus, aber im wirklichen Leben werden Sie bald eine lange Reihe von Typen haben, die sich gegenseitig stoßen können, einige davon vielleicht nur in eine Richtung.
Was
dark_orange_node
ist mit oderblack_and_orange_striped_node
oderdotted_node
? Kann es Punkte in verschiedenen Farben haben? Was ist, wenn die meisten Punkte orange sind, kann es dann gestoßen werden?Und jedes Mal, wenn Sie eine neue Regel hinzufügen müssen, müssen Sie alle
poke_adjacent
Funktionen erneut aufrufen und weitere if-Anweisungen hinzufügen.Wie immer ist es schwierig, generische Beispiele zu erstellen, das gebe ich Ihnen.
Aber wenn ich dieses spezielle Beispiel machen würde, würde ich
poke()
allen Klassen ein Mitglied hinzufügen und einige von ihnen den Aufruf (void poke() {}
) ignorieren lassen, wenn sie nicht interessiert sind.Sicher wäre das noch günstiger als der Vergleich der
typeid
s.quelle
supportsBehaviour
undinvokeBehaviour
jede Klasse kann eine Liste von Verhaltensweisen haben. Ein Verhalten wäre Poke und könnte von allen Klassen, die pokeable sein möchten, zur Liste der unterstützten Verhaltensweisen hinzugefügt werden.Ich glaube, Sie haben solche Argumente falsch verstanden.
Es gibt eine Reihe von C ++ - Codierungsstellen, an denen RTTI nicht verwendet werden soll. Wo Compiler-Switches verwendet werden, um RTTI zwangsweise zu deaktivieren. Wenn Sie innerhalb eines solchen Paradigmas codieren ... dann wurden Sie mit ziemlicher Sicherheit bereits über diese Einschränkung informiert.
Das Problem liegt daher bei Bibliotheken . Wenn Sie also eine Bibliothek schreiben, die von RTTI abhängt, kann Ihre Bibliothek dies nicht von Benutzern verwendet werden, die RTTI deaktivieren. Wenn Sie möchten, dass Ihre Bibliothek von diesen Personen verwendet wird, kann RTTI nicht verwendet werden, auch wenn Ihre Bibliothek auch von Personen verwendet wird, die RTTI verwenden können. Ebenso wichtig ist, dass Sie, wenn Sie RTTI nicht verwenden können, etwas härter nach Bibliotheken suchen müssen, da die Verwendung von RTTI für Sie ein Deal-Breaker ist.
Es gibt viele Dinge, die Sie in Hot Loops nicht tun. Sie weisen keinen Speicher zu. Sie durchlaufen keine verknüpften Listen. Und so weiter. RTTI kann sicherlich eine andere dieser "Mach das hier nicht" Dinge sein.
Berücksichtigen Sie jedoch alle Ihre RTTI-Beispiele. In allen Fällen haben Sie ein oder mehrere Objekte eines unbestimmten Typs und möchten eine Operation ausführen, die für einige von ihnen möglicherweise nicht möglich ist.
Daran muss man bei einem Design arbeiten - Ebene. Sie können Container schreiben, die keinen Speicher zuweisen, der in das "STL" -Paradigma passt. Sie können verknüpfte Listendatenstrukturen vermeiden oder deren Verwendung einschränken. Sie können Arrays von Strukturen in Strukturen von Arrays oder was auch immer reorganisieren. Es ändert einige Dinge, aber Sie können es unterteilt halten.
Eine komplexe RTTI-Operation in einen regulären virtuellen Funktionsaufruf umwandeln? Das ist ein Designproblem. Wenn Sie das ändern müssen, dann ist es etwas, das Änderungen an jedem erfordert abgeleiteten Klasse . Es ändert, wie viel Code mit verschiedenen Klassen interagiert. Der Umfang einer solchen Änderung geht weit über die leistungskritischen Codeabschnitte hinaus.
Also ... warum hast du es von Anfang an falsch geschrieben?
Zu welchem Ende?
Sie sagen, dass die Basisklasse "schlank und gemein" ist. Aber wirklich ... es gibt es nicht . Es macht eigentlich nichts .
Schauen Sie sich einfach Ihr Beispiel an :
node_base
. Was ist es? Es scheint eine Sache zu sein, die neben anderen Dingen steht. Dies ist eine Java-Schnittstelle (vorgenerisches Java): eine Klasse, die nur existiert, um etwas zu sein, das Benutzer in den realen Typ umwandeln können . Vielleicht fügen Sie einige grundlegende Funktionen wie Adjazenz hinzu (Java fügt hinzuToString
), aber das war's.Es gibt einen Unterschied zwischen "schlank und gemein" und "transparent".
Wie Yakk sagte, beschränken sich solche Programmierstile auf die Interoperabilität, denn wenn sich alle Funktionen in einer abgeleiteten Klasse befinden, können Benutzer außerhalb dieses Systems, die keinen Zugriff auf diese abgeleitete Klasse haben, nicht mit dem System zusammenarbeiten. Sie können virtuelle Funktionen nicht überschreiben und neue Verhaltensweisen hinzufügen. Sie können diese Funktionen nicht einmal aufrufen .
Aber was sie auch tun, ist, dass es ein großer Schmerz ist, tatsächlich neue Dinge zu tun, selbst innerhalb des Systems. Betrachten Sie Ihre
poke_adjacent_oranges
Funktion. Was passiert, wenn jemand einenlime_node
Typ möchte, der genau wieorange_node
s gestoßen werden kann ? Nun, wir können nicht ableitenlime_node
ausorange_node
; das macht keinen Sinn.Stattdessen müssen wir eine neue
lime_node
Ableitung von hinzufügennode_base
. Dann ändern Sie den Namen vonpoke_adjacent_oranges
inpoke_adjacent_pokables
. Und dann versuchen Sie, zuorange_node
und zu gießenlime_node
; Welche Besetzung auch immer funktioniert, wir stoßen sie an.Jedoch,
lime_node
braucht es eigenepoke_adjacent_pokables
. Und diese Funktion muss die gleichen Casting-Prüfungen durchführen.Und wenn wir einen dritten Typ hinzufügen, müssen wir nicht nur eine eigene Funktion hinzufügen, sondern auch die Funktionen in den beiden anderen Klassen ändern.
Offensichtlich machst du jetzt
poke_adjacent_pokables
eine kostenlose Funktion, so dass sie für alle funktioniert. Aber was passiert wohl, wenn jemand einen vierten Typ hinzufügt und vergisst, ihn dieser Funktion hinzuzufügen?Hallo, stiller Bruch . Das Programm scheint mehr oder weniger in Ordnung zu sein, ist es aber nicht. Wäre
poke
eine tatsächliche virtuelle Funktion gewesen, wäre der Compiler fehlgeschlagen, wenn Sie die reine virtuelle Funktion von nicht überschrieben hättennode_base
.Mit Ihrem Weg haben Sie keine solchen Compilerprüfungen. Sicher, der Compiler sucht nicht nach nicht reinen Virtuals, aber zumindest haben Sie Schutz in Fällen, in denen Schutz möglich ist (dh es gibt keine Standardoperation).
Die Verwendung transparenter Basisklassen mit RTTI führt zu einem Wartungsalptraum. In der Tat führen die meisten Anwendungen von RTTI zu Wartungsproblemen. Das bedeutet nicht, dass RTTI nicht nützlich ist (es ist
boost::any
zum Beispiel wichtig, um Arbeit zu machen). Aber es ist ein sehr spezialisiertes Werkzeug für sehr spezielle Bedürfnisse.Auf diese Weise ist es genauso "schädlich" wie
goto
. Es ist ein nützliches Werkzeug, das nicht abgeschafft werden sollte. Aber seine Verwendung sollte in Ihrem Code selten sein .Wenn Sie also keine transparenten Basisklassen und kein dynamisches Casting verwenden können, wie vermeiden Sie fette Schnittstellen? Wie verhindern Sie, dass jede Funktion, die Sie für einen Typ aufrufen möchten, vom Sprudeln bis zur Basisklasse sprudelt?
Die Antwort hängt davon ab, wofür die Basisklasse gedacht ist.
Transparente Basisklassen wie verwenden
node_base
nur das falsche Werkzeug für das Problem. Verknüpfte Listen werden am besten von Vorlagen verarbeitet. Der Knotentyp und die Nachbarschaft würden durch einen Vorlagentyp bereitgestellt. Wenn Sie einen polymorphen Typ in die Liste aufnehmen möchten, können Sie dies tun. Verwenden Sie einfachBaseClass*
alsT
im Vorlagenargument. Oder Ihr bevorzugter Smart Pointer.Es gibt aber auch andere Szenarien. Einer ist ein Typ, der viele Dinge tut, aber einige optionale Teile hat. Eine bestimmte Instanz implementiert möglicherweise bestimmte Funktionen, eine andere nicht. Das Design solcher Typen bietet jedoch normalerweise eine richtige Antwort.
Die "Entity" -Klasse ist ein perfektes Beispiel dafür. Diese Klasse hat Spieleentwickler längst geplagt. Konzeptionell hat es eine gigantische Schnittstelle, die an der Schnittstelle von fast einem Dutzend völlig unterschiedlicher Systeme lebt. Und verschiedene Entitäten haben unterschiedliche Eigenschaften. Einige Entitäten haben keine visuelle Darstellung, daher tun ihre Rendering-Funktionen nichts. Und das alles wird zur Laufzeit festgelegt.
Die moderne Lösung hierfür ist ein komponentenartiges System.
Entity
ist lediglich ein Behälter mit einer Reihe von Komponenten, zwischen denen sich etwas Klebstoff befindet. Einige Komponenten sind optional. Eine Entität ohne visuelle Darstellung verfügt nicht über die Komponente "Grafik". Eine Entität ohne KI hat keine "Controller" -Komponente. Und so weiter.Entitäten in einem solchen System sind nur Zeiger auf Komponenten, wobei der größte Teil ihrer Schnittstelle durch direkten Zugriff auf die Komponenten bereitgestellt wird.
Die Entwicklung eines solchen Komponentensystems erfordert, dass in der Entwurfsphase erkannt wird, dass bestimmte Funktionen konzeptionell zusammengefasst sind, sodass alle Typen, die eines implementieren, sie alle implementieren. Auf diese Weise können Sie die Klasse aus der potenziellen Basisklasse extrahieren und zu einer separaten Komponente machen.
Dies hilft auch dabei, das Prinzip der Einzelverantwortung zu befolgen. Eine solche komponentenbasierte Klasse hat nur die Verantwortung, Inhaber von Komponenten zu sein.
Von Matthew Walton:
OK, lass uns das untersuchen.
Damit dies sinnvoll ist, müssten Sie eine Situation haben, in der eine Bibliothek L einen Container oder einen anderen strukturierten Datenhalter bereitstellt. Der Benutzer kann diesem Container Daten hinzufügen, seinen Inhalt durchlaufen usw. Die Bibliothek macht jedoch nichts mit diesen Daten. es verwaltet einfach seine Existenz.
Aber es verwaltet nicht einmal seine Existenz, sondern seine Zerstörung . Der Grund dafür ist, dass Sie, wenn Sie RTTI für solche Zwecke verwenden sollen, Klassen erstellen, von denen L nichts weiß. Dies bedeutet, dass Ihr Code das Objekt zuweist und es zur Verwaltung an L übergibt.
Nun gibt es Fälle, in denen so etwas ein legitimes Design ist. Ereignissignalisierung / Nachrichtenübermittlung, threadsichere Arbeitswarteschlangen usw. Das allgemeine Muster lautet hier: Jemand führt einen Dienst zwischen zwei Codeteilen aus, der für jeden Typ geeignet ist, aber der Dienst muss die spezifischen Typen nicht kennen .
In C wird dieses Muster geschrieben
void*
, und seine Verwendung erfordert viel Sorgfalt, um ein Brechen zu vermeiden. In C ++ wird dieses Muster geschriebenstd::experimental::any
(wird bald geschriebenstd::any
).Dies sollte so funktionieren, dass L eine
node_base
Klasse bereitstellt, die eine nimmtany
, die Ihre tatsächlichen Daten darstellt. Wenn Sie die Nachricht, das Arbeitselement der Thread-Warteschlange oder was auch immer Sie tun, empfangen, wandeln Sie dieseany
in den entsprechenden Typ um, den sowohl der Absender als auch der Empfänger kennen.Anstatt also Ableitung
orange_node
ausnode_data
, kleben Sie einfach ein imorange
Inneren vonnode_data
‚sany
Mitgliedsfeld. Der Endbenutzer extrahiert es undany_cast
konvertiert es inorange
. Wenn die Besetzung fehlschlägt, war es nichtorange
.Wenn Sie mit der Implementierung von überhaupt vertraut sind
any
, werden Sie wahrscheinlich sagen: "Hey, warte eine Minute: Verwendetany
intern RTTI, um zu machenany_cast
Arbeit ." Worauf ich antworte: "... ja".Das ist der Punkt einer Abstraktion . Tief im Detail verwendet jemand RTTI. Auf der Ebene, auf der Sie operieren sollten, sollten Sie jedoch keine direkte RTTI durchführen.
Sie sollten Typen verwenden, die Ihnen die gewünschte Funktionalität bieten. Schließlich wollen Sie RTTI nicht wirklich. Was Sie wollen, ist eine Datenstruktur, die einen Wert eines bestimmten Typs speichern, vor allen außer dem gewünschten Ziel verbergen und dann wieder in diesen Typ konvertieren kann, wobei überprüft wird, ob der gespeicherte Wert tatsächlich von diesem Typ ist.
Das heißt
any
. Es verwendet RTTI, aber die Verwendungany
ist der direkten Verwendung von RTTI weit überlegen, da es der gewünschten Semantik besser entspricht.quelle
Wenn Sie eine Funktion aufrufen, ist es Ihnen in der Regel egal, welche genauen Schritte sie ausführen wird, nur dass ein bestimmtes übergeordnetes Ziel innerhalb bestimmter Einschränkungen erreicht wird (und wie die Funktion dies ermöglicht, ist wirklich ein eigenes Problem).
Wenn Sie RTTI verwenden, um eine Vorauswahl für spezielle Objekte zu treffen, die einen bestimmten Job ausführen können, während andere im selben Satz dies nicht können, brechen Sie diese komfortable Sicht auf die Welt. Plötzlich soll der Anrufer wissen, wer was tun kann, anstatt seinen Schergen einfach zu sagen, dass sie damit weitermachen sollen. Einige Leute sind davon betroffen, und ich vermute, dass dies ein großer Teil des Grundes ist, warum RTTI als etwas schmutzig angesehen wird.
Gibt es ein Leistungsproblem? Vielleicht, aber ich habe es noch nie erlebt, und es könnte Weisheit von vor zwanzig Jahren sein, oder von Leuten, die ehrlich glauben, dass die Verwendung von drei Montageanweisungen anstelle von zwei inakzeptabel ist.
So gehen Sie damit um ... Abhängig von Ihrer Situation kann es sinnvoll sein, knotenspezifische Eigenschaften in separaten Objekten zu bündeln (dh die gesamte 'orangefarbene' API kann ein separates Objekt sein). Das Stammobjekt könnte dann eine virtuelle Funktion haben, um die 'orange' API zurückzugeben, wobei für nicht orange Objekte standardmäßig nullptr zurückgegeben wird.
Abhängig von Ihrer Situation kann dies zwar zu viel des Guten sein, aber Sie können auf Stammebene abfragen, ob ein bestimmter Knoten eine bestimmte API unterstützt, und in diesem Fall Funktionen ausführen, die für diese API spezifisch sind.
quelle
C ++ basiert auf der Idee der statischen Typprüfung.
[1] RTTI, dh
dynamic_cast
undtype_id
ist eine dynamische Typprüfung.Sie fragen sich also im Wesentlichen, warum die statische Typprüfung der dynamischen Typprüfung vorzuziehen ist. Und die einfache Antwort lautet: Ob die statische Typprüfung der dynamischen Typprüfung vorzuziehen ist, hängt davon ab . Auf viel. C ++ ist jedoch eine der Programmiersprachen, die auf der Idee der statischen Typprüfung basieren. Dies bedeutet, dass z. B. der Entwicklungsprozess, insbesondere das Testen, in der Regel an die statische Typprüfung angepasst wird und dann am besten dazu passt.
Re
Sie können diesen prozessheterogenen Knoten eines Graphen mit statischer Typprüfung und ohne jegliches Casting über das Besuchermuster ausführen, z. B.: z.
Dies setzt voraus, dass der Satz von Knotentypen bekannt ist.
Wenn dies nicht der Fall ist, kann das Besuchermuster (es gibt viele Variationen davon) mit wenigen zentralisierten oder nur einer einzigen Besetzung ausgedrückt werden.
Einen vorlagenbasierten Ansatz finden Sie in der Boost Graph-Bibliothek. Traurig zu sagen, dass ich damit nicht vertraut bin, ich habe es nicht benutzt. Ich bin mir also nicht sicher, was genau und wie es funktioniert und inwieweit es die statische Typprüfung anstelle von RTTI verwendet. Da Boost jedoch im Allgemeinen vorlagenbasiert ist und die statische Typprüfung die zentrale Idee ist, werden Sie das wahrscheinlich finden Die Graph-Unterbibliothek basiert ebenfalls auf der statischen Typprüfung.
[1] Informationen zum Laufzeittyp .
quelle
Natürlich gibt es ein Szenario, in dem Polymorphismus nicht helfen kann: Namen.
typeid
Ermöglicht den Zugriff auf den Namen des Typs, obwohl die Art und Weise, wie dieser Name codiert wird, implementierungsdefiniert ist. Aber normalerweise ist dies kein Problem, da Sie zweitypeid
-s vergleichen können :Gleiches gilt für Hashes.
schädlich ist definitiv übertrieben: RTTI hat einige Nachteile, aber es tut Vorteile.
Sie müssen RTTI nicht wirklich verwenden. RTTI ist ein Werkzeug zur Lösung von OOP-Problemen: Sollten Sie ein anderes Paradigma verwenden, würden diese wahrscheinlich verschwinden. C hat kein RTTI, funktioniert aber trotzdem. C ++ unterstützt stattdessen OOP vollständig und bietet Ihnen mehrere Tools, um einige Probleme zu beheben, für die möglicherweise Laufzeitinformationen erforderlich sind: Eines davon ist in der Tat RTTI, das jedoch mit einem Preis verbunden ist. Wenn Sie es sich nicht leisten können, was Sie besser erst nach einer sicheren Leistungsanalyse sagen sollten, gibt es immer noch die alte Schule
void*
: Es ist kostenlos. Kostet weniger. Sie erhalten jedoch keine Typensicherheit. Es geht also nur um Trades.Wenn Sie (möglicherweise streng) konformen C ++ - Code schreiben, können Sie unabhängig von der Implementierung dasselbe Verhalten erwarten. Standardkonforme Implementierungen müssen Standard-C ++ - Funktionen unterstützen.
Bedenken Sie jedoch, dass in einigen von C ++ definierten Umgebungen («freistehende») RTTI nicht bereitgestellt werden muss und auch keine Ausnahmen
virtual
usw. RTTI benötigt eine zugrunde liegende Ebene, um korrekt zu funktionieren, die Details auf niedriger Ebene wie den ABI und die tatsächlichen Typinformationen behandelt.In diesem Fall stimme ich Yakk in Bezug auf RTTI zu. Ja, es könnte verwendet werden; aber ist es logisch korrekt? Die Tatsache, dass die Sprache es Ihnen ermöglicht, diese Prüfung zu umgehen, bedeutet nicht, dass dies durchgeführt werden sollte.
quelle