Warum ist "reiner Polymorphismus" der Verwendung von RTTI vorzuziehen?

107

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?

mbr0wn
quelle
9
Was Sie (als Sprachfunktion) "vermissen", um Ihr Beispiel für Sackorangen zu lösen, wäre Mehrfachversand ("Multimethoden"). Daher könnte die Suche nach Möglichkeiten zur Emulation eine Alternative sein. Normalerweise wird daher das Besuchermuster verwendet.
Daniel Jour
1
Die Verwendung eines Strings als Typ ist nicht sehr hilfreich. Die Verwendung eines Zeigers auf eine Instanz einer "Typ" -Klasse würde dies beschleunigen. Aber dann machen Sie im Grunde manuell, was RTTI tun würde.
Daniel Jour
4
@MargaretBloom Nein, RTTI steht für Runtime Type Information, während CRTP nur für Vorlagen steht - also für statische Typen.
Edmz
2
@ mbr0wn: Alle Engineering-Prozesse sind an einige Regeln gebunden. Programmierung ist keine Ausnahme. Die Regeln können in zwei Bereiche unterteilt werden: weiche Regeln (SOLLTEN) und harte Regeln (MUSS). (Es gibt sozusagen auch einen Ratgeber / Options-Bucket (KÖNNTE).) Lesen Sie, wie der C / C ++ - Standard (oder ein anderer englischer Standard) diese definiert. Ich denke, Ihr Problem beruht auf der Tatsache, dass Sie "RTTI nicht verwenden" als harte Regel verwechselt haben ("Sie dürfen RTTI NICHT verwenden"). Es ist eigentlich eine weiche Regel ("Sie sollten RTTI nicht verwenden"), was bedeutet, dass Sie sie nach Möglichkeit vermeiden
3
Ich node_basestelle 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 vornehmen node_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).
Matthew Walton

Antworten:

69

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:

class node_base {
  public:
    bool has_tag(tag_name);

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.

Yakk - Adam Nevraumont
quelle
14
+1 für den allerletzten Absatz; nicht nur, weil ich Ihnen zustimme, sondern weil es hier der Hammer auf den Nagel ist.
7
Wie gelangt man zu einer bestimmten Funktionalität, wenn man weiß, dass ein Objekt diese Funktionalität unterstützt? Entweder beinhaltet dies das Casting, oder es gibt eine Gottklasse mit jeder möglichen Mitgliedsfunktion. Die erste Möglichkeit ist entweder ungeprüftes Casting. In diesem Fall ist das Tagging nur ein eigenes, sehr fehlbares dynamisches Typprüfungsschema, oder es ist geprüft 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.
Prost und hth. - Alf
2
@ Falco: Das ist (eine Variante von) die erste Möglichkeit, die ich erwähnt habe, ungeprüftes Casting basierend auf dem Tag. Hier ist das Markieren das eigene sehr spröde und sehr fehlbare dynamische Typprüfungsschema. Jedes kleine Fehlverhalten des Client-Codes, und in C ++ ist eines in UB-Land deaktiviert. Sie erhalten keine Ausnahmen wie in Java, sondern undefiniertes Verhalten wie Abstürze und / oder falsche Ergebnisse. Es ist nicht nur äußerst unzuverlässig und gefährlich, sondern auch äußerst ineffizient im Vergleich zu vernünftigerem C ++ - Code. IOW., Es ist sehr sehr gut; extrem so.
Prost und hth. - Alf
1
Ähm. :) Argumenttypen?
Prost und hth. - Alf
2
@JojOatXGME: Weil "Polymorphismus" bedeutet, mit einer Vielzahl von Typen arbeiten zu können. Wenn Sie überprüfen müssen, ob es sich um einen bestimmten Typ handelt, über die bereits vorhandene Typprüfung hinaus, mit der Sie den Zeiger / die Referenz erhalten haben, schauen Sie hinter den Polymorphismus. Sie arbeiten nicht mit verschiedenen Typen. Sie arbeiten mit einem bestimmten Typ. Ja, es gibt "(große) Projekte in Java", die dies tun. Aber das ist Java ; Die Sprache erlaubt nur dynamischen Polymorphismus. C ++ hat auch statischen Polymorphismus. Nur weil jemand "groß" ist, ist es keine gute Idee.
Nicol Bolas
31

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:

  • Durch Goedelisierung auf eine Dimension reduzieren : Hier befinden sich virtuelle Basen und Mehrfachvererbung mit Diamanten und gestapelten Parallelogrammen. Dies setzt jedoch voraus, dass die Anzahl der möglichen Kombinationen bekannt und relativ klein ist.
  • Verketten Sie die Dimensionen ineinander (wie im Muster der zusammengesetzten Besucher, aber dies erfordert, dass alle Klassen sich ihrer anderen Geschwister bewusst sind, sodass sie nicht von dem Ort "skalieren" können, an dem sie konzipiert wurden).
  • Versenden Sie Anrufe basierend auf mehreren Werten. Genau dafür ist RTTI da.

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, gotonur um es zu verprügeln. Dinge für Papageien, keine Programmierer.

Emilio Garavaglia
quelle
Eine gute Darstellung der Ebenen, auf denen jeder Ansatz anwendbar ist. Ich habe allerdings noch nichts von "Goedelization" gehört - ist es auch unter einem anderen Namen bekannt? Könnten Sie vielleicht einen Link oder eine weitere Erklärung hinzufügen? Danke :)
j_random_hacker
1
@j_random_hacker: Auch ich bin neugierig auf diese Verwendung von Godelization. Normalerweise betrachtet man die Godelisierung als erstens die Zuordnung von einer Zeichenfolge zu einer Ganzzahl und zweitens die Verwendung dieser Technik, um selbstreferenzielle Anweisungen in formalen Sprachen zu erstellen. Ich bin mit diesem Begriff im Zusammenhang mit dem virtuellen Versand nicht vertraut und würde gerne mehr erfahren.
Eric Lippert
1
In der Tat bin ich zu missbrauchen des Begriffs: nach Goedle, da jeder ganzen Zahl entspricht ein ganze Zahl n-ple (die Kräfte der seine Primfaktoren) und jede n-ple entspricht eine ganze Zahl, jedes diskretes n-dimensionale Indizierung Problem kann sein auf ein eindimensionales reduziert . Das bedeutet nicht, dass dies der einzige Weg ist, dies zu tun: Es ist nur ein Weg zu sagen, "es ist möglich". Alles was Sie brauchen ist ein "Teilen und Erobern" -Mechanismus. Virtuelle Funktionen sind die "Teilung" und Mehrfachvererbung ist die "Eroberung".
Emilio Garavaglia
... Wenn alles, was in einem endlichen Feld (einem Bereich) passiert, Linearkombinationen effektiver sind (das klassische i = r * C + c erhält den Index in einem Array der Zelle einer Matrix). In diesem Fall ist die Teilung der "Besucher" und der Eroberer der "Verbund". Da es sich um lineare Algebra handelt, entspricht die Technik in diesem Fall der "Diagonalisierung"
Emilio Garavaglia
Denken Sie nicht an all dies als Techniken. Sie sind nur Analogien
Emilio Garavaglia
23

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_nodeist mit oder black_and_orange_striped_nodeoder dotted_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_adjacentFunktionen 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 typeids.

Bo Persson
quelle
3
Sie sagen "sicher", aber was macht Sie so sicher? Das versuche ich wirklich herauszufinden. Angenommen, ich benenne orange_node in pokable_node um und sie sind die einzigen, bei denen ich poke () aufrufen kann. Das bedeutet, dass meine Schnittstelle eine poke () -Methode implementieren muss, die beispielsweise eine Ausnahme auslöst ("dieser Knoten ist nicht pokbar"). Das scheint mehr teuer.
mbr0wn
2
Warum sollte er eine Ausnahme werfen müssen? Wenn Sie sich darum gekümmert haben, ob die Schnittstelle "poke-fähig" ist oder nicht, fügen Sie einfach eine Funktion "isPokeable" hinzu und rufen Sie sie zuerst auf, bevor Sie die Poke-Funktion aufrufen. Oder tun Sie einfach, was er sagt, und "tun Sie nichts in nicht stechbaren Klassen".
Brandon
1
@ mbr0wn: Die bessere Frage ist, warum pokable und nichtpokable Knoten dieselbe Basisklasse verwenden sollen.
Nicol Bolas
2
@NicolBolas Warum sollten freundliche und feindliche Monster dieselbe Basisklasse oder fokussierbare und nicht fokussierbare UI-Elemente oder Tastaturen mit einem Nummernblock und Tastaturen ohne Nummernblock verwenden?
user253751
1
@ mbr0wn Das klingt nach Verhaltensmuster. Die Basisschnittstelle verfügt über zwei Methoden supportsBehaviourund invokeBehaviourjede 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.
Falco
20

Einige Compiler verwenden es nicht / RTTI ist nicht immer aktiviert

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 kostet zusätzlichen Speicher / Kann langsam sein

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?

Ich muss keine Attribute oder Methoden definieren, bei denen ich sie nicht benötige. Die Basisknotenklasse kann schlank und gemein bleiben.

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_orangesFunktion. Was passiert, wenn jemand einen lime_nodeTyp möchte, der genau wie orange_nodes gestoßen werden kann ? Nun, wir können nicht ableiten lime_nodeaus orange_node; das macht keinen Sinn.

Stattdessen müssen wir eine neue lime_nodeAbleitung von hinzufügen node_base. Dann ändern Sie den Namen vonpoke_adjacent_oranges in poke_adjacent_pokables. Und dann versuchen Sie, zu orange_nodeund zu gießen lime_node; Welche Besetzung auch immer funktioniert, wir stoßen sie an.

Jedoch, lime_node braucht es eigene poke_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 pokeeine 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::anyzum 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_basenur 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 einfach BaseClass*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. Entityist 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:

Ich stelle fest, dass viele Antworten nicht die Idee haben, dass Ihr Beispiel darauf hindeutet, dass node_base Teil einer Bibliothek ist und Benutzer ihre eigenen Knotentypen erstellen. Dann können sie node_base nicht ändern, um eine andere Lösung zuzulassen. Vielleicht wird RTTI dann ihre beste Option.

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 geschrieben std::experimental::any(wird bald geschriebenstd::any ).

Dies sollte so funktionieren, dass L eine node_baseKlasse bereitstellt, die eine nimmt any, die Ihre tatsächlichen Daten darstellt. Wenn Sie die Nachricht, das Arbeitselement der Thread-Warteschlange oder was auch immer Sie tun, empfangen, wandeln Sie diese anyin den entsprechenden Typ um, den sowohl der Absender als auch der Empfänger kennen.

Anstatt also Ableitung orange_nodeaus node_data, kleben Sie einfach ein im orangeInneren von node_data‚s anyMitgliedsfeld. Der Endbenutzer extrahiert es und any_castkonvertiert es in orange. Wenn die Besetzung fehlschlägt, war es nicht orange.

Wenn Sie mit der Implementierung von überhaupt vertraut sind any, werden Sie wahrscheinlich sagen: "Hey, warte eine Minute: Verwendet any 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 Verwendung anyist der direkten Verwendung von RTTI weit überlegen, da es der gewünschten Semantik besser entspricht.

Nicol Bolas
quelle
10

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.

H. Guijt
quelle
6
Betreff: die Leistungskosten - Ich habe dynamic_cast <> in unserer App auf einem 3-GHz-Prozessor als ungefähr 2µs gemessen, was ungefähr 1000x langsamer ist als das Überprüfen einer Aufzählung. (Unsere App hat eine Frist von 11,1 ms für die Hauptschleife, daher kümmern wir uns sehr um Mikrosekunden.)
Crashworks
6
Die Leistung ist zwischen den Implementierungen sehr unterschiedlich. GCC verwendet einen Typeinfo-Zeigervergleich, der schnell ist. MSVC verwendet Zeichenfolgenvergleiche, die nicht schnell sind. Die MSVC-Methode funktioniert jedoch mit Code, der mit verschiedenen Versionen von Bibliotheken (statisch oder DLL) verknüpft ist. Die Zeigermethode von GCC geht davon aus, dass sich eine Klasse in einer statischen Bibliothek von einer Klasse in einer gemeinsam genutzten Bibliothek unterscheidet.
Zan Lynx
1
@Crashworks Nur um hier eine vollständige Aufzeichnung zu haben: Welcher Compiler (und welche Version) war das?
H. Guijt
@Crashworks unterstützt die Anfrage nach Informationen darüber, welcher Compiler Ihre beobachteten Ergebnisse erzielt hat; Vielen Dank.
underscore_d
@underscore_d: MSVC.
Crashworks
9

C ++ basiert auf der Idee der statischen Typprüfung.

[1] RTTI, dh dynamic_castund type_idist 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

Ich würde keinen sauberen Weg kennen, dies mit Vorlagen oder anderen Methoden zu tun

Sie können diesen prozessheterogenen Knoten eines Graphen mit statischer Typprüfung und ohne jegliches Casting über das Besuchermuster ausführen, z. B.: z.

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

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 .

Prost und hth. - Alf
quelle
1
Eine "lustige Sache" ist, dass man die Menge an Code (Änderungen beim Hinzufügen von Typen) reduzieren kann, die für das Besuchermuster erforderlich ist, indem man RTTI verwendet, um eine Hierarchie zu "erklimmen". Ich kenne das als "azyklisches Besuchermuster".
Daniel Jour
3

Natürlich gibt es ein Szenario, in dem Polymorphismus nicht helfen kann: Namen. typeidErmö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 zwei typeid-s vergleichen können :

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

Gleiches gilt für Hashes.

[...] RTTI gilt als "schädlich"

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.


  • Einige Compiler verwenden 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.

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 virtualusw. 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.

edmz
quelle