dynamic_cast

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 und reinterpret_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 Pointern 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.

try
{
  switch(state)
  {
  case state_1:
    {
      ((derived_type1_t*)data)->do_something();
      break;
	}
  case state_2:
    {
      ((derived_type2_t*)data)->do_something();
      break;
	}
  }
}
catch(...)
{
}

Mein Änderung sah dann so aus:

try
{
  switch(state)
  {
  case state_1:
    {
      derived_type1_t* mydata = dynamic_cast<derived_type1_t*>(data);
	  if(!mydata) throw std::runtime_error("Invalid data");
      mydata->do_something();
      break;
	}
  case state_2:
    {
      derived_type2_t* mydata = dynamic_cast<derived_type2_t*>(data);
      if(!mydata) throw std::runtime_error("Invalid data");
      mydata->do_something();
      break;
	}
  }
}
catch(...)
{
}

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 kommte 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!


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!