Variant in C++ 98/03

Mit std::variant aus C++17 hat die standardbibliothek 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, soetwas “quick & dirty” auch dort nachbauen zu können.


Mit C++11 kamen “variadic Tempates” 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ütten, 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:

 1union
 2{
 3  char  t0[sizeof(T0)];
 4  char  t1[sizeof(T1)];
 5  char  t2[sizeof(T2)];
 6  char  t3[sizeof(T3)];
 7  char  t4[sizeof(T4)];
 8  char  t5[sizeof(T5)];
 9  char  t6[sizeof(T6)];
10  char  t7[sizeof(T7)];
11  char  t8[sizeof(T8)];
12  char  t9[sizeof(T9)];
13  void* alignment_dummy;
14} content;

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-Desktruktor 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 Funktionspointer 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:

1... 
2class Variant
3{
4private:
5  generic_copy_constructor content_cctor;
6  generic_destructor content_dtor;
7  std::type_info const* content_ti;
8};

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.