C++ RTTI: Run-Time Type Information

Die Run-Time Type Information (kurz RTTI) ist eines dieser Features in C++, weshalb mache Anwender auf Exceptions und virtuelle Methoden verzichten.

Denn während viele andere Features der Sprache genau “abgeschätzt” werden können, eröffnet RTTI einen Baum von Abhängigkeiten, die schnell auch zu ungewolltem Overhead werden können.


Erstmals bewusst gemacht wurde mir RTTI mit Studio 2005 und Windows CE. Denn während RTTI in jeder Standard-C++-Umgebung aktiviert ist (und bewusst deaktiviert werden müsste), werden CE-Programme ohne RTTI Support gebaut, womit dann keine einzige C++ Exception durch den Compiler durchkommt.

RTTI ist also diese geheime Kraft, die dynamic_cast möglich macht, das catchen von typisierten Exception bewirkt und dabei jede Menge kryptischen Code erzeugt, wenn man einen Blick auf den Assembler-Output wirft.

Up-Casting (also Casts von einer Ableitung zur Basisklasse) ist in C++ meistens “kostenlos”, weil Objekte von der Basis zur Ableitung hin konstruiert werden.

Wird ein Derived* Pointer zu Base* ist die Speicheradresse meist identisch, nur im Falle von Mehrfachvererbung wird ein statischer Offset addiert, weil jedes Derived Objekt seine Base enthalten muss.

Umgekehrt weiß ein Base* nicht, ob es ursprünglich mal als Derived* erzeugt wurde.
Die einzige Möglichkeit dies herauszufinden ist die v-Table bei Objekten mit virtuellen Methoden.

Der Compiler generiert daher für jedes Klasse mit virtuellen Methoden eine v-Table und verknüpft diese mit anderen v-Table Einträgen in internen Datenstrukturen.
So kann dann ein dynamic_cast beim Aufruf nachsehen, welche v-Table mit dem Objekt verknüpft ist und findet darüber heraus, ob der gewünschte Datentyp in der Hierarchie mit dem originalen Typen verbunden ist.
Wenn ja, wird ein neuer Pointer + Typ erzeugt, falls nein fliegt std::bad_cast bei Referenzen oder man erhält einen Null-Pointer zurück.

Bei Exceptions geschieht etwas Ähnliches, hier wird die Typeninformation “mitgeworfen”, damit der Code dann den richtigen catch Handler findet, wenn der Stack bis zu einem solchen zurückgebaut wurde.

Problem: Runtime

Mein größtes Problem mit RTTI ist, dass diese Typen-Information auf Daten und Strukturen der C++-Runtime-Library zurückgreift.
Der Compiler generiert also nicht nur eigenen Code, sondern setzt auch das Vorhandensein einiger weiterer Funktionen im Binary voraus, die nicht “dynamisch” generiert werden.

Beim MSVC betrifft das vor allem das Exception Handling. Das ist in regulären Windows Programmen auch kein Problem, weil diese immer mit der C-Runtime verknüpft sind.

Es gibt aber andere Orte, wo das zum Problem wird, und das wären z.B.:

  • die Treiber Entwicklung
  • oder das Erzeugen von UEFI Apps

Denn diese Kompilate enthalten keine C-Runtime und dürfen diese wegen ihrer Bindung zum OS-Usermode auch nicht beinhalten.

Und das ärgert mich. Denn der Compiler hätte die Möglichkeit, den Code generisch zu gestalten und auf die externe Runtime-Library zu verzichten.
Er tut das aber, um mit der Windows-API kompatibel zu sein und um das Structured Exception Handling zu nutzen.

Hier hätte ich mir einen Compiler-Switch erhofft, mit dem man auf “Native Exceptions” oder so etwas wechseln kann.

Aber vielleicht kommt das ja noch, denn seit ein paar Jahren geistert die Idee der Zero-Overhead-Exceptions durch die Community.

Neuer Ansatz: Zero-overhead deterministic exceptions.

Herb Sutter hat diese Vorschlag eingereicht, und er stellt damit “meine Welt” schon etwas auf den Kopf … doch wenn ich darüber nachdenke, wird mir die Idee immer sympathischer.

Wir lernten:

Throw by value, catch by reference.

Und dieses catch by reference baut ebenso auf RTTI auf, wenn jede Exception von std::exception erben soll. Folglich kann catch(std::exception&) eine jede fangen.

Hier braucht man dann zusätzlichen Code und Heap-Speicher, um die Exception zu werfen. throw muss also auf new vertrauen, was leider auch schief gehen kann.

Der andere Ansatz ist jedoch:

Wir fangen alle Exception by value.

Und das bedeutet, dass in der Funktion, in welcher der catch Block steht schon speicher allokiert werden kann, um die Exception darauf abbilden zur können. Und wenn das schon der Fall ist, können wir auch weiter optimieren und eine neue Exception schon während throw genau dort konstruieren lassen, wo sie letztendlich gefangen werden soll.

Wow, das wäre die Lösung. Denn wenn “mein Framework” keine polymorphen Klassen mehr einsetzt und per Referenz fängt, dann könnte ein statisches Programme alle Ausnahmen so generieren lassen, dass überhaupt keine dynamische Allokierung oder RTTI Auflösung mehr notwendig sind.

Fazit

Leider sind alle diese theoretischen Überlegungen noch ferne Zukunftsmusik. Denn Compiler Hersteller brauchen oft einige Jahre, um neue Features einzufügen.

Ein Verzicht auf RTTI ist also heute für mich nicht denkbar und beim MSVC leider ausgeschlossen.

Ein bisschen Hoffnung hege ich noch beim GCC, ob man den so hinbekommen kann, dass er Exceptions und RTTI Auflösungen so umsetzt, dass keine externen CRT Funktionen notwendig sind.

Mal sehen, was daraus wird, doch bis dahin bleibt C mein Favorit in Sachen EFI.