C++ RTTI: Run-Time Type Information
« | 02 Apr 2022 | »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.