Variant in C++ 98/03
« | 17 Jul 2022 | »Mit std::variant
aus C++17
hat die st
and
ardbibliothek
boost::variant
integriert. Das Template ist mit C union
Typen verwandt, bietet jedoch
Typensicherheit und Eindeutigkeit.
Ich bin bekanntlich auf die alten Standards C++98 und C++03 fixiert und fragte mich daher, wie schwer es wohl sei, so etwas “quick & dirty” auch dort nachbauen zu können.
Mit C++11 kamen “variadic Templates” in den Standard womit man beliebig viele Typen an ein Template weiterreichen kann. Und diese kann man dann mit anderen Hilfstemplates durchlaufen und sich genau das daraus bauen, was man haben will.
Doch im originalen C++98 bzw. im Update 2003 gibt es dieses Feature nicht.
Idee
“Default template arguments” geben uns die Möglichkeit, ein Template mit Typen zu füttern, doch wenn wir weniger nutzen, werden die hinten fehlenden Argumente durch “defaults” ersetzt.
Meine Idee ist es daher ein Variant
Template mit vielen Typen zu erstellen
(10 sollten für die meisten Fälle ausreichen), wo jedoch ab dem zweiten ein
Default-Typ gesetzt wird, der das Template Argument quasi als “ungültig”
markiert.
Damit die “ungültigen” Argumente auch eindeutig identifiziert bzw. gezählt
werden können, kann dieser InvalidType
selbst ein Template sein, dessen
Argument eine Zahl ist:
1template<unsigned N> 2struct InvalidType { }; 3 4template<class T0, 5 class T1 = InvalidType<1>, 6 class T2 = InvalidType<2>, 7 class T3 = InvalidType<3>, 8 class T4 = InvalidType<4>, 9 class T5 = InvalidType<5>, 10 class T6 = InvalidType<6>, 11 class T7 = InvalidType<7>, 12 class T8 = InvalidType<8>, 13 class T9 = InvalidType<9>, 14 > class Variant { };
Im Inneren reservieren wir Speicherplatz für alle möglichen Typen mit:
Jetzt kann man mit new (&content) Tx(source)
bereits Objekte auf den
content
Speicher erzeugen … aber wie soll der Destruktor nun das
richtige Objekt freigeben?
Im GATE Framework nutze ich für die C++-C Brücken C-Konstruktor und C-Destruktor Funktionen, die von C++ Templates generiert werden.
1typedef void(*generic_copy_constructor)(void* dst, void const* src); 2typedef void(*generic_destructor)(void* obj); 3 4template<class T> 5void cpp_copy_construct(void* dst, void const* src) 6{ 7 T* cpp_dst = reinterpret_cast<T*>(dst); 8 T const* cpp_src = reinterpret_cast<T const*>(src); 9 new (cpp_dst) T(*cpp_src); 10} 11 12template<class T> 13void cpp_destruct(void* obj) 14{ 15 T* cpp_obj = reinterpret_cast<T*>(obj); 16 cpp_obj->~T(); 17}
Diese beiden Funktionszeiger kommen zusammen mit der std::type_info
ebenfalls in den Variant und beschreiben somit, welcher Type
gerade aktiv ist, und wie man ihn kopieren und zerstören kann:
Jetzt baut man noch Konstruktoren und Assignment Operatoren für jeden Template Typen ein, wie auch einen “getter”.
1... 2private: 3 template<class T> 4 void insert(T const& t) 5 { 6 new (&content) T(t); 7 content_cctor = &generic_copy_constructor<T>; 8 content_dtor = &cpp_destruct; 9 content_ti = typeid(T); 10 } 11 void destruct() 12 { 13 content_dtor(&content); 14 } 15public: 16 Variant(T0 const& t) { insert(t) } 17 Variant(T1 const& t) { insert(t) } 18 ... 19 Variant& operator=(T8 const& t) { destruct(); insert(t); return *this; } 20 Variant& operator=(T9 const& t) { destruct(); insert(t); return *this; } 21 22 template<class T> T& get() 23 { 24 if(typeid(T) != *content_ti) 25 { 26 throw std::runtime_error("Invalid type"); 27 } 28 return *reinterpret_cast<T*>(&content); 29 }
Und fertig ist der C++98 Variant. Das ganze Beispiel liegt auch im BLOG-Classroom vor.
Natürlich kann man jetzt den Code noch weiter ausbauen und die echte
Konstruktion eines InvalidType
verhindern lassen, damit man ihn nicht
(un)absichtlich deklarieren kann.
Und man kann sich auch noch den Typen-Index merken, mit dem der Variant
initialisiert wurde, um ihn (so wie bei std::variant
) zurückliefern
zu können.
Fazit
Das Codebeispiel ist nicht so effizient, wie es durch C++11 möglich wird, doch er tut so halbwegs das, was man von einem “Variant” erwartet.
Ich selbst nutze variant
eher selten, doch kommt er mir in manch anderen
Projekten stets unter.
Im GATE Projekt geht die ganze Sache natürlich noch einen Schritt weiter,
wo ein gate_variant_t
als C-Objekt genutzt wird, wo aber dennoch ein C++
Objekt dahinter verborgen sein kann. Dort hat man keine Templates und muss
sich quasi alle typeid
-Pointer mit den cctor/dtor Funktionen für jeden
erlaubten Typen merken.
Und wieder gilt als erwiesen:
Wo ein Wille ist, da ist auch ein Weg.