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 catch
en 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.