MSVC 6/7 type traits
« | 05 Feb 2022 | »Mit dem Visual Studio 2005 wurde der MSVC halbwegs ordentlich nutzbar. Zwar nicht alle, aber zumindest viele C++ Template-Konstruktionen wurden mit dieser Version endlich standard-konform umgesetzt und so konnten boost und andere Bibliotheken langsam in Richtung C++11 marschieren.
Doch … was war vorher?
Tatsächlich dokumentieren ältere boost
Ausgaben einige faszinierende
Workarounds um Template-Features wie Type-Traits auch mit älteren
nicht-standard-konformen Compilern umzusetzen.
Keine Partial template specialization
Template hatten in MSVC 6 und 7 zahlreiche Macken. Eine einfaches generisches Template funktionierte, doch sobald man Spezialisierungen einsetze, wurde es kompliziert.
Gar nicht implementiert waren Teilspezialisierungen, um Typen-Attribute filtern
zu können.
Ein sehr bekanntes Beispiel wäre die Pointer-Erkennung:
Ein alter MSVC
würde hier mit einem Syntaxfehler abbrechen, weil es den
Token is_pointer<T*>
nicht übersetzen könnte.
Lösung per Funktionsüberladung
Im MSVC
können wir aber einen ganz anderen Ansatz nutzen.
Wir definieren eine Funktion, die den “gewünschten” Typen oder einen
angepassten Wrapper mit dem Typen entgegennimmt und die als Rückgabetyp
einen spezifischen Typ nutzt.
Und dann überladen wir die Funktion mit einer zweiten Implementierung,
die alles entgegennehmen kann (also eine variadic function) und die einen
anderen Rückgabetyp definiert.
Ich übernehme hier gerne die boost
Konvention mit einem yes_type
und
einen no_type
, denn die eigentliche Entscheidung, wie ein Typ klassifiziert
wird, erfolgt eben über diesen Rückgabetyp.
Wenn wir nämlich einen solchen überladenen Funktionsaufruf “andeuten”,
ermittelt der Compiler, welche der Funktionen am besten passt, womit der
Rückgabetyp der Funktion entweder mit yes
oder mit no
typisiert wird.
Und aus dieser Info lässt sich dann wieder eine Type-Trait Konstante bilden,
wenn man die Rückgabetypen mit sizeof
vergleicht.
Das seltsame dabei ist, dass der “angedeutete” Funktionsaufruf nie stattfindet und die Funktionen selbst gar nicht implementiert werden müssen.
Beispiel is_pointer
1yes_type_t pointer_detector(void const* ptr); 2no_type_t pointer_detector(...); 3 4template<class T> struct is_pointer_helper 5{ 6 static T& type_creator(); 7 enum { 8 value = (sizeof(yes_type_t) == 9 sizeof(pointer_detector(type_creator())) 10 }; 11} 12 13template<class T> struct is_pointer 14{ 15 enum { 16 value = is_pointer_helper<T>::value 17 } 18};
Jeder Pointer lässt sich implizit in void const*
casten, womit jeder
Pointer die erste yes
Funktion nutzen würde. Alle anderen Typen landen bei
der no
Funktion.
(Für eine vollständige Implementierung müssten wir jetzt auch noch
Fälle wie void volatile*
usw. abdecken, aber das Prinzip sollte klar sein).
Beispiel is_reference
Referenzen sind im MSVC leider wieder ein Spezialfall, weil der Compiler
(nicht gedeckt durch den Standard) das Nutzen von Funktionsrückgabewerten
als nicht-konstante Parameter erlaubt.
int foo(int& param)
kann also auch mit einem int bar()
Aufruf gefüttert
werden, obwohl der Rückgabewerte keine Referenz ist. Andere Compiler würden
das als Fehler ablehnen.
Folglich können wir die is_pointer
Methode nicht einfach auf Referenzen
anpassen.
Die Kollegen des boost
Teams haben sich dafür etwas Besonderes einfallen
lassen:
Sie definieren eine Funktion, die zu einem Typ T
eine Funktion als Parameter
erwartet, die selbst wieder ein T&
zurückgibt.
Aus jeder Nicht-Referenz kann eine Referenz gemacht werden, aber es kann keine
Referenz zu einer Referenz geben.
Und damit das mit Function-Overloading funktioniert, müssen wir statt unseres
Typen T
einen Hilfstypen typed_dummy
einführen, der als Parameter fungieren
kann:
1template<class T> struct typed_dummy { }; 2 3template <class T> 4T& (* ref_func_returner(typed_dummy<T>) ) (typed_dummy<T>); 5char ref_func_returner(...); 6 7template <class T> 8no_type_t ref_detector(T&(*)(typed_dummy<T>)); 9yes_type_t ref_detector(...); 10 11template<class T> struct is_reference_helper 12{ 13 enum { 14 value = (sizeof(yes_type_t) == 15 sizeof(ref_detector(ref_func_returner(typed_dummy<T>())))) 16 }; 17}; 18 19template<class T> struct is_reference 20{ 21 enum { 22 value = is_reference_helper<T>::value 23 } 24};
Der ref_detector
bekommt also den Rückgabewert der
ref_func_returner
Funktion als Parameter.
Ist T
eine Referenz wird das generische char ref_func_returner(...)
aufgerufen und ref_detector(...)
antwortet mit yes
.
Wenn T
keine Referenz ist, lässt sich ein T&
daraus machen, was die
Templatefunktion ins Spiel bringt, woraus am Ende ein no
wird.
Fazit
Was solche Workaround anbelangt ist boost
vor allem in den älteren
Version (z.B. 1.3x
) eine wahre Fundgrube.
Und es gibt noch jede Menge weitere Varianten und Compiler-Hacks, die das
Bereitstellen von Features ermöglichen sollen, die eigentlich mit diesen
alten Compilern nicht möglich waren.
Leider wird vieles in boost
durch eine unendliche Anzahl von Makros
umgesetzt, die (zumindest für mich) schwer lesbar sind.
Um so mehr freut man sich dafür dann aber, wenn man einen Workaround mal
verstanden hat.
Nachdem immer mehr Forenbeiträge und Blogs aus den Zeiten vor 2010 verschwinden, möchte ich zumindest ein paar dieser Ideen im GATE Projekt verewigen. Ich nutze type-traits zwar ziemlich selten … aber mit diesen Hacks bleibt die Lauffähigkeit auch auf MSVC 6 weiter gegeben.