dynamic_cast testen



  • Hallo,

    bei einem alten Compiler möchte ich in Zukunft auch RTTI, vor allem aber den dynamic_cast verwenden.

    Bisher war RTTI im Projekt deaktiviert, das SW Design ist aber höchst polymorph gehalten, ständig sieht man so tolle Casts wie:

    Basis *b=ListeVonPolymorphenObjekten[x];
    if(b->GetTyp()==eTypA)
    {
     Abgeleitet *a=static_cast<Abgeleitet*>(b);
     a->EineFunktionVonAbgeleitet(); // bummm!
    }
    

    Durch Änderungen im Code ist es öfter als einmal vorgekommen, dass mit dem static_cast auf die falsche Klasse konvertiert worden ist, dann wurde eine Funktion aufgerufen, die es natürlich garnicht gibt (weil falscher Typ), und bumm, Absturz.

    In Zukunft möchte ich zumindest in neuen Codeteilen so arbeiten, dass ich nur mit dynamic_cast von der Basis auf eine abgeleitete Klasse caste.
    Dann hab ich wenigstens die Garantie, dass ich auch tatsächlich mit dem richtigen Typen arbeite.

    Basis *b=ListeVonPolymorphenObjekten[x];
    Abgeleitet *a=dynamic_cast<Abgeleitet*>(b);
    
    if(a)
    {
     a->EineFunktionVonAbgeleitet(); 
    }
    

    Wie erwähnt, der Compiler ist alt und setzt den C++ Standard teilweise falsch um. Trotzdem müssen wir mit diesem Compiler kompatibel bleiben.
    Wie kann ich nun den Compiler auf Herz und Nieren testen, dass er mir den dynamic_cast auch korrekt durchführt?

    Ich hab mir sowas in der Art vorgestellt.
    Zur Erklärung: /GR bedeutet RTTI aktivieren, /GR- dekativieren. Die stopHere hab ich mir hingesetzt, damit ich Breakpoints setzen kann.

    Reicht der Test, oder fehlen noch irgendwelche Spezialfälle, um zu sehen, ob dynamic_cast funktioniert?

    // Klassen Hierachie:
    //    A
    //  /   \
    // B      C
    // |
    // D
    
    struct A
    {
    	virtual char GetType()
    	{
    		return 'A';
    	}
    };
    
    struct B : public A
    {
    	virtual char GetType()
    	{
    		return 'B';
    	}
    };
    
    struct C : public A
    {
    	virtual char GetType()
    	{
    		return 'C';
    	}
    };
    
    struct D : public B
    {
    	virtual char GetType()
    	{
    		return 'D';
    	}
    };
    
    void testRTTI()
    {
    	A *pA=0, *pB, *pC, *pD;
    	pA=new A;
    	pB=new B;
    	pC=new C;
    	pD=new D;
    
    	// pA auf B* konvertieren => erwartet: else
    	// Ergebnis mit /GR:
    	// Ergebnis mit /GR-:
    	if(dynamic_cast<B*>(pA))
    	{
    		int stopHere=0;
    	}
    	else
    	{
    		int stopHere=0;
    	}
    
    	// pB auf B* konvertieren => erwartet: if
    	// Ergebnis mit /GR:
    	// Ergebnis mit /GR-:
    	if(dynamic_cast<B*>(pB))
    	{
    		int stopHere=0;
    	}
    	else
    	{
    		int stopHere=0;
    	}
    
    	// pC auf B* konvertieren => erwartet: else
    	// Ergebnis mit /GR:
    	// Ergebnis mit /GR-:
    	if(dynamic_cast<B*>(pC))
    	{
    		int stopHere=0;
    	}
    	else
    	{
    		int stopHere=0;
    	}
    
    	// pD auf B* konvertieren => erwartet: if
    	// Ergebnis mit /GR:
    	// Ergebnis mit /GR-:
    	if(dynamic_cast<B*>(pD))
    	{
    		int stopHere=0;
    	}
    	else
    	{
    		int stopHere=0;
    	}
    }
    


  • rttiTester schrieb:

    Durch Änderungen im Code ist es öfter als einmal vorgekommen, dass mit dem static_cast auf die falsche Klasse konvertiert worden ist

    dynamic_cast ist hier der falsche Ansatz. Das kostet nur Performance (und nicht zu knapp!!)

    Sofern dein alter Compiler Templates unterstützt:

    template <typename T> T* basis_cast(Basis *b) {
      assert(b->getType() == T::eTypA); // check
      return static_cast<T*>(b); // safe
    }
    
    // das kann nicht mehr schiefgehen
    Basis *b=ListeVonPolymorphenObjekten[x];
    if(b->GetTyp()==eTypA)
    {
     Abgeleitet *a=basis_cast<Abgeleitet*>(b); // bummm schon hier!
     a->EineFunktionVonAbgeleitet();
    }
    


  • Mal eine vielleicht etwas naive Frage: ist so etwas wie

    b->GetTyp()
    

    nicht bereits so etwas wie RTTI, nur eben selbstgebastelt? Wie sind "echte" RTTI üblicherweise Implementiert und warum ist das langsamer?



  • TNA schrieb:

    Wie sind "echte" RTTI üblicherweise Implementiert

    Stringvergleiche und Baumtraversierung.

    und warum ist das langsamer?

    Weil das offensichtlich langsam ist.
    Libraryübergreifend geht das nicht viel besser.

    RTTI, nur eben selbstgebastelt?

    Es bieten sich Primzahlen an.

    A=(2,2)
       /        \
    B=(3,6)     C=(5,10)
      |
    D=(7,30)
    

    Die erste Zahl ist eine eindeutige Primzahl, die zweite Zahl ist das Produkt der Basisklassen.

    Teilbarkeit ist damit Teilbarkeit Variable.getID()%Klasse::primzahl == 0 => Variable ist vom Typ Klasse.



  • @auchabgeleitet: assert meldet sich nur im Debug Modus. Die Lösung muss auch beim Kunden funktionieren, da die Sache einigermaßen komplex ist, können durchaus Konstellationen auftreten, die wir im Test nicht nachstellen konnten, und dynamic_cast mag zwar langsam sein, aber wenigstens ist es sicher.

    Mal abgesehen davon hätte dynamic_cast ja auch den Vorteil, dass sowohl ein B als auch ein D auf B konvertiert wird.
    Mit dem von dir vorgeschlagenen Typvergleich erschlage ich nur den Fall, dass es sich tatsächlich um ein B handelt.

    A* pB=new B;
    A* pD=new D;

    dynamic_cast wandelt mir beide in ein B* um, während dein basis_cast ja nur bei pB, nicht aber bei pD funktioniert.

    @TNA: dynamic_cast durchwandert ganze Klassenhierachien, um zu sehen, ob der Cast ok ist. Klar ist GetType() auch eine Art von RTTI, aber dynamic_cast kann mehr.



  • rttiTester schrieb:

    @auchabgeleitet: assert meldet sich nur im Debug Modus. Die Lösung muss auch beim Kunden funktionieren, da die Sache einigermaßen komplex ist, können durchaus Konstellationen auftreten, die wir im Test nicht nachstellen konnten, und dynamic_cast mag zwar langsam sein, aber wenigstens ist es sicher.

    Es steht dir natürlich auch frei, das assert durch ein throw InvalidClassCastExecutorManagerException() zu ersetzen.

    rttiTester schrieb:

    Mal abgesehen davon hätte dynamic_cast ja auch den Vorteil, dass sowohl ein B als auch ein D auf B konvertiert wird.

    Wie habt ihr das dann bisher gemacht!?

    Ansonsten halt basis_cast spezialisieren (tag-dispatchen) um für jede Klasse einen eigenen Casting-Code zu schreiben.



  • auchabgeleitet schrieb:

    rttiTester schrieb:

    Durch Änderungen im Code ist es öfter als einmal vorgekommen, dass mit dem static_cast auf die falsche Klasse konvertiert worden ist

    dynamic_cast ist hier der falsche Ansatz. Das kostet nur Performance (und nicht zu knapp!!)

    Das ist mal wieder so ein "premature optimization". Es kommt immer darauf an, ob das wirklich relevant ist. Sicher ist dynamic_cast nicht die schnellste Möglichkeit, aber vielleicht an der Stelle die übersichtlichste und robusteste Variante. Da fallen ein paar CPU-Zyklen dann wirklich nicht ins Gewicht.



  • Meistens will man static_cast für Downcasts. Und dynamic_cast wird dann höchstens zur Kontrolle im Debug-Modus verwendet. Du kannst das auch kombinieren, wie boost::polymorphic_downcast .

    Mit if und dynamic_cast verbirgst du nur Logikfehler. Du gehst davon aus, dass sich eine Klasse downcasten lässt, wie reagierst du bei falschen Typen? Du kannst nicht reagieren. Das einzig Richtige hier ist ein assert() o.Ä., sodass dieser Fall sicher nicht durch den Debugger kommt. Einfach eine if -Abfrage und nichts tun, bedeutet Logikfehler ignorieren und Applikation in korruptem Status weiterlaufen lassen.

    auchabgeleitet schrieb:

    Weil das offensichtlich langsam ist.
    Libraryübergreifend geht das nicht viel besser.

    Warum ist typeid langsamer als getType() ?

    auchabgeleitet schrieb:

    Es steht dir natürlich auch frei, das assert durch ein throw InvalidClassCastExecutorManagerException() zu ersetzen.

    🤡

    tntnet schrieb:

    Sicher ist dynamic_cast nicht die schnellste Möglichkeit, aber vielleicht an der Stelle die übersichtlichste und robusteste Variante. Da fallen ein paar CPU-Zyklen dann wirklich nicht ins Gewicht.

    Du weisst ja nicht, wie oft die Downcasts vorkommen, dynamic_cast kann schon ins Gewicht fallen. Vor allem wenn man bedenkt, dass der Overhead bei korrektem Code nicht nötig ist. Wie gesagt ist schon der Ansatz, dynamic_cast hier verwenden zu wollen, verkehrt -- Performance ist dabei nebensächlich.



  • tntnet schrieb:

    Sicher ist dynamic_cast nicht die schnellste Möglichkeit, aber vielleicht an der Stelle die übersichtlichste und robusteste Variante.

    Stimmt, ich so gewohnt, dass GetType() nur an performance-kritischen Stellen vorkommt, dass ich stillschweigend davon ausgegangen bin.

    Übersichtliche und robuste Alternativen sind virtuelle Funktionen im Objekt oder das Visitor-Pattern.

    Nexus schrieb:

    Warum ist typeid langsamer als getType() ?

    Der Test, ob A von B erbt ist ungefähr so implementiert:

    for (auto a_type = typeid(A); a_type.name() != typeid(B).name();) {
    //                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-- das ist richtig lahm
      if (a_type.has_parent())
        a_type = a_type.parent();
      else
        return false;
    }
    return true;
    

    Jetzt vergleich das mal mit einem einfachen Funktionspointeraufruf.

    Ich gehe sogar so weit und habe ein Feld mit der ID. Dann ist das nicht mal ein Funktionsaufruf.



  • Korrigierte version:

    bool a_erbt_von_b(type_info a_type, type_info b_type) {
      if (a_type.name() == b_type.name();
        return true;
      for (type_info p : a_type.parents())
       if (a_erbt_von_b(p, b))
          return true;
      return false;
    }
    


  • ok, um es kurz zu sagen:
    klar wäre es mir auch lieber, wenn das design dementsprechend wäre, dass man ohne typinfos auskommt.
    ist aber nicht so.
    und ich kann mich auch nicht 100% darauf verlassen, dass ich immer die richtigen typen übergeben bekomme. auch nicht schön.
    ist aber so.
    ich brauche jetzt erstmal eine robuste methode, um sichere downcasts durchzuführen. und dafür ist dynamic_cast geeignet.

    also bitte keine bekehrungen im sinne von "dynamic_cast ist ungeeignet". ich werde es im ersten schritt verwenden müssen. mir ist es lieber, wenn die SW beim kunden etwas langsamer ist, als wenn es kracht.

    also daher nochmals meine ursprüngliche frage, auf die bis jetzt keine antwort kam: welche tests sind nötig, um zu sehen, ob dynamic_cast einwandfrei funktioniert? was wären "härtefälle"? reicht der test, den ich oben präsentiert habe?



  • Ich verstehe immer noch nicht, warum GetTyp() besser als typeid sein sollte. Ich wuerde naemlich den Typ in der vtable hinterlegen oder aehnlich.

    Weiterhin funktioniert die vorgestellte Loesung nur mit Blaettern in der Vererbungshierarchie. dynamic_cast beruecksichtigt auch Zwischenknoten. Deswegen wird wohl dynamic_cast auch langsamer sein als GetType/typeid, aber beide Ansaetze sind sowieso nicht aequivalent.



  • auchabgeleitet schrieb:

    Der Test, ob A von B erbt ist ungefähr so implementiert:

    warum sollte es das sein? dr COmpiler kann doch einfach einen Baum speichern der Pro Typ ienen Knoten hat und dann pro Instanz die ID des passenden Klassenknoten speichern. mit der id findest du dann den passenden Knoten im Baum und upcasten ist dann einfach nur die parents solange traversieren bis in Knoten die richtige ID hat. crosscast ist dann Breitensuche im Baum.


  • Mod

    knivil schrieb:

    Weiterhin funktioniert die vorgestellte Loesung nur mit Blaettern in der Vererbungshierarchie. dynamic_cast beruecksichtigt auch Zwischenknoten.

    und Mehrdeutigkeiten. Die Suche kann also bei einem Fund nicht sofort abgebrochen werden. Die Laufzeit eines cross-Casts hängt damit wesentlich von der Gesamtgröße der Vererbungshierarchie ab.
    Einfache Downcasts gehen nat. schneller. Was nicht hilft, wenn der Cast fehlschlägt (dann musste ja der Algorithmus für den Crosscast erst mal angewendet werden).



  • a_type.name() != typeid(B).name()
    

    Meine Güte, liest hier keiner Alexandrescu? Das funktioniert nicht! Es ist nicht garantiert dass zwei Klassen unterschiedliche " type_info "-Namen haben, und auch nicht dass der Name überhaupt gleich bleibt (in der anderen Implementierung oder auch Version)!



  • Deswegen nimmt man ja auch nicht den String sondern die Id. Stringvergleiche sind unnoetig.



  • Sone schrieb:

    a_type.name() != typeid(B).name()
    

    Meine Güte, liest hier keiner Alexandrescu?

    Viel zu viele Leute lesen Alexandrescu.

    Dir ist schon bewusst, dass das eine Beispielimplementation für dynamic_cast war? Die Compilerhersteller werden schon wsisen, ob sie kompatibel zu sich selbst sind.



  • Ich übersehe wohl was. Wozu braucht man Upcasts und Downcasts?
    Die korrekte Lösung für das hier:

    auchabgeleitet schrieb:

    Basis *b=ListeVonPolymorphenObjekten[x];
    if(b->GetTyp()==eTypA)
    {
     Abgeleitet *a=basis_cast<Abgeleitet*>(b); // bummm schon hier!
     a->EineFunktionVonAbgeleitet();
    }
    

    ist doch wohl sowas in der Art:

    struct Basis{
     virtual void rufDieRichtigeFunktionAuf(){
     }
    };
    struct Abgeleitet{
     void EineFunktionVonAbgeleitet();
     void rufDieRichtigeFunktionAuf(){
      EineFunktionVonAbgeleitet();
     }
    };
    
    ...
    
    Basis *b=ListeVonPolymorphenObjekten[x];
    b->rufDieRichtigeFunktionAuf();
    

    Jetzt, da RTTI angeschaltet ist sollte das doch funktionieren und der Compiler wird sich schon was Schlaues und Optimiertes für die Implementierung überlegen.

    Wenn man das nicht will, weil die abgeleiteten Klassen zu verschieden sind, als dass man für alles eine virtuelle Funktion anlegen will, dann sollte man vielleicht nicht alles von der Basis ableiten und in einen Container stopfen.



  • auchabgeleitet schrieb:

    for (auto a_type = typeid(A); a_type.name() != typeid(B).name();) {
    //                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-- das ist richtig lahm
    

    Macht man ja auch nicht. std::type_info (und std::type_index ) ist direkt vergleichbar. Der Vergleich kann z.B. auch mit Integern implementiert werden.

    Ich würde hier nicht zu viel spekulieren, die Performance von RTTI hängt massgeblich von der Compiler-Implementierung ab. Ausserdem hat man meist andere Probleme, wenn man RTTI benötigt.


  • Mod

    nwp3 schrieb:

    Ich übersehe wohl was. Wozu braucht man Upcasts und Downcasts?
    Die korrekte Lösung für das hier:

    auchabgeleitet schrieb:

    Basis *b=ListeVonPolymorphenObjekten[x];
    if(b->GetTyp()==eTypA)
    {
     Abgeleitet *a=basis_cast<Abgeleitet*>(b); // bummm schon hier!
     a->EineFunktionVonAbgeleitet();
    }
    

    ist doch wohl sowas in der Art:

    struct Basis{
     virtual void rufDieRichtigeFunktionAuf(){
     }
    };
    struct Abgeleitet{
     void EineFunktionVonAbgeleitet();
     void rufDieRichtigeFunktionAuf(){
      EineFunktionVonAbgeleitet();
     }
    };
    
    ...
    
    Basis *b=ListeVonPolymorphenObjekten[x];
    b->rufDieRichtigeFunktionAuf();
    

    Jetzt, da RTTI angeschaltet ist sollte das doch funktionieren und der Compiler wird sich schon was Schlaues und Optimiertes für die Implementierung überlegen.

    Wenn man das nicht will, weil die abgeleiteten Klassen zu verschieden sind, als dass man für alles eine virtuelle Funktion anlegen will, dann sollte man vielleicht nicht alles von der Basis ableiten und in einen Container stopfen.

    Das geht sogar ohne RTTI, entspricht aber nicht dem gestellten Problem, als da wäre mit Äpfeln und Birnen umzugehen ohne über Obst zu sprechen.


Log in to reply