dynamic_cast
« | 24 Sep 2019 | »Liebes Tagebuch.
Heute hat sich folgendes Gespräch ergeben:
Im letzten Release hat es noch funktioniert, aber seit deiner Änderung gibt es einen Fehler.
Kann nicht sein, ich habe den Code nur ein bisschen verbessert, sonst wurde nichts verändert.
Ich habe mal eine interessante Aussage zu den *_cast
Routinen in
C++ gelesen, die beinhaltete:
static_cast
,dynamic_cast
,const_cast
undreinterpret_cast
sind nur deshalb so umständlich lange und hässlich ausformuliert worden, damit C++ Programmierer sie so selten wie möglich nutzen … schließlich sollten casts in der objektorientierten Programmierung eigentlich gar nicht mehr notwendig sein.
Tja … dieser Schuss ist dann aber gehörig nach hinten losgegangen.
Denn viele C++ Programmierer nutzen nun die kürzeren C-style-Casts.
Und die sind deshalb so übel, weil der Compiler je nach Fall einen der
obigen Casts daraus ableitet, wobei deren Bedeutung unterschiedlich
sein kann.
Bei Zahlen bedeutet ein C-Cast meist einen static_cast
, bei Zeigern
kommt aber oft ein reinterpret_cast
am Ende heraus.
Das wirklich üble dabei: Es kompiliert immer und erzeugt (meist) keine
Warnungen, wenn Datentypen “seltsam” interpretiert werden.
reinterpret vs. dynamic
Neulich hatte ich so einen lustigen Fall, wo obiges Gespräch dabei herauskam.
Tatsächlich hatte ich bei bestehenden Codes die C-Casts durch entsprechende
C++-Casts ersetzt, und in erste Linie auf das Abfangen von NULL-Pointer
abgezielt.
Der hinzugefügte dynamic_cast
bei einem Shared-Pointer
auf ein Basis-Klassen-Interface, wo eine spezielle Methode einer speziellen
Ableitung notwendig war, war einfach als eine “Draufgabe” von mir gedacht,
so wie es das Lehrbuch verlangt hatte.
Hmm, und nun läuft produktive Software nicht mehr richtig, weil
der dynamic_cast
einen Null-Pointer liefert, der erkannt wird und
aus Sicherheit mit einer Fehlermeldung abgebrochen wird.
Warum hat es also früher funktioniert?
In dieser Software gibt es viele unterschiedliche Ableitungen eines Basis-Interfaces, die alle sehr unterschiedlich sind. Doch zwei bestimmte Ableitungen sind im Code (und eigentlich auch binär-) identisch, sie sollen aber zwei unterschiedliche Zustände ausdrücken.
Mein Änderung sah dann so aus:
1try 2{ 3 switch(state) 4 { 5 case state_1: 6 { 7 derived_type1_t* mydata = dynamic_cast<derived_type1_t*>(data); 8 if(!mydata) throw std::runtime_error("Invalid data"); 9 mydata->do_something(); 10 break; 11 } 12 case state_2: 13 { 14 derived_type2_t* mydata = dynamic_cast<derived_type2_t*>(data); 15 if(!mydata) throw std::runtime_error("Invalid data"); 16 mydata->do_something(); 17 break; 18 } 19 } 20} 21catch(...) 22{ 23}
Kurz zusammengefasst: Der Ersteller der data
-Instanz hat in allen Fällen
immer nur derived_type1_t
benutzt. Der frühere C-style Cast wurde
zu einem reinterpret_cast
, der nur aus Gottes Gnaden und durch Zufall
funktioniert hat, weil derived_type1_t
and derived_type2_t
gleich
aufgebaut waren.
Fazit
Das Schlimme dabei ist, dass schon darüber diskutiert wurde die korrekte Änderung (ja, sie war korrekter!) von mir wieder zurückzunehmen, weil eine Änderung des Ursprungscodes in einer anderen Abteilung hätte durchgeführt werden müssen und offenbar “schwer umsetzbar” gewesen wäre.
Um das zu verhindern erlaubte ich in beiden Fällen einfach beide Datentypen. Geht der eine nicht, wird der zweite probiert.
Hoffentlich kommt jetzt morgen nicht die Meldung, dass noch ein dritter (ebenso unkorrekter) Datentyp zurückkommen kann …
Tja, und deshalb sehe ich mich gezwungen stets neunmalklug immer und immer wieder zur wiederholen:
Wenn von einer Basisklasse mit mindestens einer virtuellen Methode auf eine Ableitung gecastet werden soll, MUSS das immer über einen abgesicherten
dynamic_cast
erfolgen, damit man Typenfehler korrekt erkennen kann.
Ach ja, und noch viel wichtiger ist folgender Merksatz.
Bugs sind IMMER an ihrer Wurzel zu fixen und sollen nicht per Workaround drei Ebenen weiter zaghaft entschärft werden müssen!