Variant Typen und All-in-one Variablen
« | 04 May 2019 | »Der größte Vorteil von statisch typisierten Sprachen wie C oder C++ kann auch ein Nachteil werden:
Was ist, wenn der Typ einer Variablen erst zur Laufzeit z.B. durch eine
Benutzereingabe oder eine Fremdbibliothek festgelegt werden kann?
Oder ein anderes Beispiel: Wie kann man eine Variable bauen, die aus
unterschiedlichen Datenbankabfragen unterschiedliche Inhalte von Boolean bis
String beinhalten können soll?
Das alles schaffen “variierende” Typen.
Wenn man sich an Microsoft’s COM orientiert, ist alles ganz einfach:
Man baut eine struct
und nennt sie VARIANT
, gibt ihr einen Integer
Member namens vt
(für VARTYPE
) und dann einen union
Member, der
alle primitiven Datentypen und auch einige Pointer zu komplexeren Typen
beinhaltet.
Nun orientiert man sich am Wert von vt
, denn dieser legt fest, welcher
union
Member gerade gültig ist.
So funktioniert das auch bei anderen Implementierungen: Es gibt einen Typen-Identifizierer (Typen-ID) und dann einen mehr oder weniger variablen Speicherbereich, wo der tatsächliche Typ abgebildet wird.
Stellt sich bloß noch die Frage, wo die Typen-ID herkommt.
Nun, COM ist deshalb limitiert, weil es die primitiven Typen
(bool, int, string) eine endliche Menge sind und alle Objekte mit dem Interface
IUnknown
beginnen müssen. Daher reicht für alle Objekte ein Pointer of
IUnknown
und der Nutzer darf sich per QueryInterface
sein bevorzugtes
Interface vom Objekt abholen.
In C++ lässt sich dank Templates so ziemlich alles in eine “generische” Struktur pressen und die Sprache liefert die ID gleich mit:
Für jeden (und zwar wirklich jeden) möglichen C++ Typ liefert der
typeid
Operator
eine Instanz von std::type_info
zurück, der mit anderen verglichen
werden kann.
Ein einfacher “variierender” Typ ist also schnell selbst gebaut:
1class Variant 2{ 3private: 4 std::type_info info; 5 void* instance; 6 ... 7 8public: 9 template<class T> Variant(T const& source) 10 : info(typeid(source)), instance(new T(source)) 11 { 12 } 13 14 template<class T> bool get(T& copy) const 15 { 16 if(typeid(copy) == this->info) 17 { 18 copy = *static_cast<T*>(this->instance); 19 return true; 20 } 21 else 22 { 23 return false; 24 } 25 } 26 ... 27};
Dem Codebeispiel fehlen natürlich noch Vorkehrungen auch das Kopieren und das Löschen der Instanz vorzunehmen, doch auch das kann man mit Templates auf Funktionen abbilden, deren Pointer dann ebenfalls in den Variant wandern und bei Bedarf aufgerufen werden.
Fazit:
Was ist also besser?
- Für jeden unterstützen Typen eine ID in Form einer Zahl generieren?
- Oder einfach
typeid
benutzen?
Die Antwort lautet (wie leider so oft): Es hängt davon ab…
Für reine C++ Projekt ist die zweite Variante ein großer Vorteil.
Auch boost::variant
baut darauf auf.
Doch wenn es um den Austausch von Daten mit anderen Bibliotheken geht, wird automatisch der erste Ansatz zum besseren. Denn mit einer C-Struktur mit einem Integer als erstem Member kann wirklich jede andere Sprache umgehen und dadurch, dass der “Variant-Type” von Hand spezifiziert wird, liegt es auch in der Hand des Entwicklers, wie weit die Kompatibilität reichen soll.
Im Fall von C++ typeid
sind Compiler- und Prozessgrenzen eingezogen.
Es ist nicht garantiert, dass ein typeid aus einer DLL mit dem typeid
einer EXE kompatibel ist und je nach Version und Header-Stand kann es hier
zu Problemen und Mehrdeutigkeiten kommen.
In diesem Fall lautet meine Empfehlung: VARIANTen immer von Hand definieren (und sich natürlich dann auch an die Definitionen halten!). Ist zwar mehr Aufwand, funktioniert dann aber über viele Jahrzehnte (wenn es sein muss).