
Zusammenfassung der vorherigen Teile
Aufgrund von Einschränkungen bei der Verwendung von C ++ 11-Compilern und mangelnder Alternativen wollte boost seine eigene Implementierung der Standard-C ++ 11-Bibliothek über die mit dem Compiler gelieferte C ++ 98 / C ++ 03-Bibliothek schreiben.
Static_assert ,
noexcept ,
countof wurden implementiert, und nach Berücksichtigung aller nicht standardmäßigen
Definitionen und Compilerfunktionen wurden Informationen zu den vom aktuellen Compiler unterstützten Funktionen
angezeigt . Eine eigene Implementierung von
nullptr ist enthalten , die bei der Kompilierung ausgewählt wird.
Es ist Zeit für
type_traits und all diese "besondere Vorlagenmagie". In den vorherigen Teilen dieses Kapitels haben wir meine Implementierung der grundlegenden Vorlagen der Standardbibliothek untersucht. In diesem Teil werden wir über die Kombination der SFINAE-Technik mit Vorlagen und ein wenig über die Codegenerierung sprechen.
Link zu GitHub mit dem Ergebnis für heute für ungeduldige und Nichtleser:
Engagements und konstruktive Kritik sind willkommen
Weitere C ++ - Vorlagen unter Kat.-Nr.
Inhaltsverzeichnis
EinführungKapitel 1. Viam Supervadet VadensKapitel 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endifKapitel 3. Finden der perfekten nullptr-ImplementierungKapitel 4. C ++ Template Magic....
4.1 Wir fangen klein an....
4.2 Über wie viele wundersame Fehler das Protokoll für uns kompiliert....
4.3 Zeiger und alles in allem....
4.4 Was wird sonst noch für die Vorlagenbibliothek benötigt?Kapitel 5
...
Kapitel 4. Vorlage "magic" C ++. Fortsetzung
4.3 Zeiger und alles in allem
Zu diesem Zeitpunkt konnte ich nur Informationen darüber erhalten, ob der Typ ein Array für
std :: is_array ist, und es war möglich, Vorlagen für Zeiger zu starten. Die Implementierung war ebenfalls trivial, jedoch nicht ohne Annahmen.
Eine einfache Vorlagenspezialisierung für Arrays einer bestimmten Länge "fängt" alle Arten von Arrays ab. Das Problem tritt jedoch beim unvollständigen Typ
T [] auf (ein Array ohne Angabe der Länge). Tatsache ist, dass dieser Typ von einigen Compilern (C ++ Builder) bei der Spezialisierung einer Vorlage nicht definiert wird, und ich habe hier noch keine universelle Lösung gefunden.
Nachdem der Bibliothek „beigebracht“ wurde, integrierte Typen zu definieren, im Typenspeicher auszurichten, mit Typmodifikatoren und anderen grundlegenden Dingen über Vorlagen zur Kompilierungszeit zu arbeiten, war es Zeit für Zeiger und Referenzen.

In C ++ können zwei Gruppen von Zeigern unterschieden werden - Zeiger auf Klassenmitglieder und Zeiger auf andere Objekte. Warum ist diese Trennung für die weitere Implementierung der Standardbibliothek wichtig? Tatsache ist, dass Zeiger auf Klassenmitglieder einen signifikanten Unterschied zu anderen Zeigern aufweisen, wenn
dies vorhanden ist , d. H. Zeiger auf ein Objekt dieser Klasse. Standardmäßig haben Zeiger auf ein Klassenmitglied eine separate Syntax zum Definieren, sind ein separater Typ und können nicht durch einen regulären Zeiger dargestellt werden. In der Praxis bedeutet dies, dass die Größe eines Zeigers auf ein Klassenmitglied normalerweise größer ist als die Größe eines regulären Zeigers (der
== sizeof (void *) ), weil
Um virtuelle Elementfunktionen der Klasse zu implementieren und
diesen Zeiger zu speichern, implementieren Compiler normalerweise Zeiger auf ein Klassenelement als Struktur (Informationen
zu virtuellen Funktionen und
Strukturen ). Die Art und Weise, wie Zeiger den Klassenmitgliedern präsentiert werden, liegt gemäß dem Standard im Ermessen des Compilers, aber wir werden diesen Unterschied in Größe und Darstellung berücksichtigen, wenn wir weiteren Code in Betracht ziehen.
Um einen regulären Zeiger auf ein Objekt zu definieren, schreiben wir eine einfache
is_pointer- Vorlage sowie eine
is_lvalue_reference- Vorlage für Objektreferenzen (
wir haben
den is_rvalue_reference
beiseite gelegt, da es bis zum 11. Standard keinen
&& -Operator sowie die gesamte Verschiebungssemantik gab):
namespace detail { template<class> struct _is_pointer_helper : public false_type { }; template<class _Tp> struct _is_pointer_helper<_Tp*> : public true_type { }; }
Hier gibt es nichts grundlegend Neues mehr, trotzdem wurde es in den vorherigen Teilen dieses Kapitels getan. Lassen Sie uns weiterhin Zeiger auf Objekte definieren - schauen wir uns nun Zeiger auf Funktionen an.
Es ist wichtig zu verstehen, dass eine Funktion und eine Elementfunktion einer Klasse gemäß dem Standard völlig unterschiedliche Entitäten sind:
- Der erste Zeiger ist normal (ein Zeiger auf ein Objekt), der zweite hat einen Zeiger auf ein Klassenmitglied.
void (*func_ptr)(int);
- Sie können einen Link zum ersten (Objektlink) erstellen, aber keinen zweiten Link.
void (&func_ref)(int);
Hier möchte ich nur ein wenig über die Codegenerierung sprechen. Da es vor C ++ 11 keine Vorlagen mit einer variablen Anzahl von Parametern gab, wurden alle Vorlagen, bei denen es eine andere Anzahl von Parametern geben konnte
, durch Spezialisierung der Hauptvorlage mit einer großen Anzahl von Parametern am Eingang und deren Initialisierung durch Standard-Dummy-Parameter bestimmt. Dasselbe gilt für Funktionsüberlastungen wie Es gab auch keine Makros mit einer variablen Anzahl von Parametern. Da Sie 60-70 Zeilen derselben Art von Vorlagenspezialisierungen mit Ihren Händen schreiben, ist das Überladen von Funktionen eine ziemlich langweilige und nutzlose Aufgabe, und es ist auch mit der Möglichkeit behaftet, einen Fehler zu machen. Ich habe für diese Zwecke einen einfachen Codegenerator für Vorlagen und Funktionsüberladungen geschrieben. Ich habe mich darauf beschränkt, Funktionen auf 24 Parameter zu definieren, und dies sieht im Code ziemlich umständlich aus, ist aber einfach und klar:
namespace detail { template <class R> struct _is_function_ptr_helper : false_type {}; template <class R > struct _is_function_ptr_helper<R(*)()> : true_type {}; template <class R > struct _is_function_ptr_helper<R(*)(...)> : true_type {}; template <class R, class T0> struct _is_function_ptr_helper<R(*)(T0)> : true_type {}; template <class R, class T0> struct _is_function_ptr_helper<R(*)(T0 ...)> : true_type {};
... template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24> struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24)> : true_type {}; template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24> struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 ...)> : true_type {}; }
Wir definieren die Typen, die uns aus dem vorherigen Kapitel für die SFINAE-Technik bekannt sind:
namespace detail {
Noch ein paar Makros namespace detail { #define _IS_MEM_FUN_PTR_CLR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const volatile); #ifdef _STDEX_CDECL _no_type _STDEX_CDECL _is_mem_function_ptr(...); #define _IS_MEM_FUN_CDECL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const volatile); #define _IS_MEM_FUN_STDCALL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const volatile); #define _IS_MEM_FUN_FASTCALL_PTR \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS)); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) volatile); \ template <class R, class T TYPES > \ _yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const volatile); #else _no_type _is_mem_function_ptr(...); #define _IS_MEM_FUN_CDECL_PTR #define _IS_MEM_FUN_STDCALL_PTR #define _IS_MEM_FUN_FASTCALL_PTR #endif #define _IS_MEM_FUN_PTR \ _IS_MEM_FUN_PTR_CLR \ _IS_MEM_FUN_CDECL_PTR \ _IS_MEM_FUN_STDCALL_PTR \ _IS_MEM_FUN_FASTCALL_PTR }
Makros werden so definiert, dass es relativ bequem ist,
TYPEN und
ARGS-Definitionen als Liste von Typen und Parametern neu zu definieren und dann das Makro
_IS_MEM_FUN_PTR zu ersetzen, um Definitionen für alle möglichen Funktionstypen durch den Präprozessor
zu generieren. Beachten Sie auch, dass für Compiler von Microsoft auch
Anrufvereinbarungen (
__fastcall ,
__stdcall und
__cdecl )
wichtig sind , weil Bei unterschiedlichen Konventionen sind die Funktionen unterschiedlich, obwohl sie dieselben Argumente und denselben Rückgabewert haben. Infolgedessen wird dieses ganze grandiose Makrodesign ziemlich kompakt verwendet:
namespace detail { #define TYPES #define ARGS _IS_MEM_FUN_PTR #undef TYPES #undef ARGS #define TYPES , class T0 #define ARGS T0 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS #define TYPES , class T0, class T1 #define ARGS T0, T1 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS
... #define TYPES , class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24 #define ARGS T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 _IS_MEM_FUN_PTR #undef TYPES #undef ARGS
Und nun zu dem, was alles geschrieben wurde:
namespace detail { template <class _Tp, bool _IsRef> struct _is_mem_function_ptr_impl { static _Tp *p; static const bool value = (sizeof(_is_mem_function_ptr(_is_mem_function_ptr_impl::p)) == sizeof(_yes_type)); typedef typename integral_constant<bool, _is_mem_function_ptr_impl::value == bool(true)>::type type; }; template <class _Tp> struct _is_mem_function_ptr_impl<_Tp, true>: public false_type {}; template <class _Tp> struct _is_mem_function_ptr_helper: public _is_mem_function_ptr_impl<_Tp, is_reference<_Tp>::value>::type {}; template <class _Tp, bool _IsMemberFunctionPtr> struct _is_function_chooser_impl : public false_type { }; template <class _Tp> struct _is_function_chooser_impl<_Tp, false> : public _is_function_ptr_helper<_Tp*> { }; template<class _Tp, bool _IsRef = true> struct _is_function_chooser : public false_type { }; template <class _Tp> struct _is_function_chooser<_Tp, false> { static const bool value = _is_function_chooser_impl<_Tp, _is_mem_function_ptr_helper<_Tp>::value>::value; }; }
Um zu überprüfen, ob ein Typ eine Mitgliedsfunktion einer Klasse ist, wird zunächst geprüft, ob der Typ eine Referenz ist. Dann wird ein Zeiger dieses Typs erstellt und in die Sondenfunktion eingesetzt. Unter Verwendung der SFINAE-Technik wählt der Compiler die erforderliche Überladung von Sondenfunktionen für einen solchen Zeiger aus und bildet basierend auf dem Ergebnis des Vergleichs mit
_yes_type das Ergebnis.
Basierend auf einer Prüfung einer Mitgliedsfunktion einer Klasse wird eine Typprüfung darauf geschrieben, ob sie zum Funktionstyp gehört. Wir prüfen, ob der Typ eine Referenz ist. Wenn nicht, suchen wir nach einer geeigneten Spezialisierung der Template-Probe-Strukturen für einen Zeiger dieses Typs, der für alle Funktionszeiger mit bis zu 24 Parametern
true_type ist.
Und jetzt verwenden wir das Ergebnis, um
is_function zu implementieren. Aus dem gleichen Grund wie im
vorherigen Teil konnte ich diese Struktur hier nicht von
Integral_Konstante erben, daher wird das Verhalten von
Integral_Konstante "simuliert".
Und für die Implementierung von
is_member_function_pointer ist es noch einfacher:
Anhand dieser Muster können wir außerdem feststellen, ob der Typ im Prinzip ein Mitglied der Klasse ist:
namespace detail { template<class _Tp> struct _is_member_object_pointer_impl1 : public _not_< _or_<_is_function_ptr_helper<_Tp>, _is_mem_function_ptr_helper<_Tp> > >::type { }; template<class _Tp> struct _is_member_object_pointer_impl2 : public false_type { }; template<class _Tp, class _Cp> struct _is_member_object_pointer_impl2<_Tp _Cp::*> : public true_type { }; template<class _Tp> struct _is_member_object_pointer_helper: public _and_<_is_member_object_pointer_impl1<_Tp>, _is_member_object_pointer_impl2<_Tp> >::type {}; }
Verwendete logische Operationen 'und', 'oder', 'nicht' für Typen aus dem ersten Teil namespace detail { struct void_type {};
Hier verwenden wir logische Operationen für Typen, die mithilfe der
bedingten Vorlage schließlich den entsprechenden Vorlagentyp auswählen. Die Vorlagenprogrammierung in ihrer ganzen Pracht, daher haben wir bereits in der Kompilierungsphase Informationen darüber, ob der Typ ein Mitglied der Klasse ist. Ziemlich "wütend", aber wie spektakulär und effektiv!
Eine etwas
reinere Vorlagenprogrammierung auf denselben logischen Elementen und wir haben
is_fundamental ,
is_compound usw. Zeichen (das freut mich, aber Sie?):
Ein aufmerksamer Leser wird feststellen, dass die Definition von is_enum auskommentiert ist . Tatsache ist, dass ich keine Möglichkeit gefunden habe, Enum von anderen Typen zu unterscheiden, aber ich denke, dass dies ohne die Verwendung von Compiler-abhängigen Makros möglich ist. Vielleicht sagt Ihnen ein aufmerksamer und sachkundiger Leser Ihren Weg oder Gedankengang in dieser Hinsicht.
Um festzustellen, dass ein Typ eine Klasse ist, wird jetzt nichts mehr benötigt:
namespace detail { template <class _Tp, bool _IsReference> struct _is_class_helper { typedef integral_constant<bool, false> type; }; template <class _Tp> struct _is_class_helper<_Tp, false> { typedef integral_constant<bool, (is_scalar<_Tp>::value == bool(false))
Und alles wäre in Ordnung, aber
Union in C ++ kann im allgemeinen Fall nicht von einer Klasse unterschieden werden. Weil sie sich in ihren "externen Manifestationen" sehr ähnlich sind und ich die Unterschiede (zum Beispiel die Unfähigkeit, von
Union zu erben) ohne Kompilierungsfehler nicht überprüfen konnte. Vielleicht sagt Ihnen jemand ein schwieriges Manöver, um die
Vereinigung bei der Kompilierung zu bestimmen, dann entspricht
is_class genau dem Standard.
Im
letzten Teil dieses Kapitels werde ich darüber sprechen, wie
std :: zerfall und
std :: common_type implementiert wurden und was noch zu
type_traits hinzugefügt werden
muss .
Danke für die Aufmerksamkeit.