MSVC 6/7 type traits

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:

 1template<class T> class is_pointer
 2{
 3  static bool const value = false;
 4};
 5template<class T> class is_pointer<T*>
 6{
 7  static bool const value = true;
 8}
 9int main()
10{
11  assert(false == is_pointer<int>::value);
12  assert(true == is_pointer<int*>::value);
13}

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.

1typedef char yes_type_t;
2typedef struct { char n[2]; } no_type_t;

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.