Variant Typen und All-in-one Variablen

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:

my_type_t my_instance;
std::type_info my_type_id = typeid(my_instance);

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:

class Variant
{
private:
	std::type_info	info;
	void*			instance;
	...
	
public:
	template<class T> Variant(T const& source)
	: info(typeid(source)), instance(new T(source))
	{		
	}
	
	template<class T> bool get(T& copy) const
	{
		if(typeid(copy) == this->info)
		{
			copy = *static_cast<T*>(this->instance);
			return true;
		}
		else
		{
			return false;
		}
	}
	...
};

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?

  1. Für jeden unterstützen Typen eine ID in Form einer Zahl generieren?
  2. 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).


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!