Wie habe ich die Standard-C ++ 11-Bibliothek geschrieben oder warum ist Boost so beängstigend? Kapitel 4.3

Wir setzen das Abenteuer fort.

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ührung
Kapitel 1. Viam Supervadet Vadens
Kapitel 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Kapitel 3. Finden der perfekten nullptr-Implementierung
Kapitel 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.

// is_array template<class> struct is_array : public false_type { }; template<class _Tp, std::size_t _Size> struct is_array<_Tp[_Size]> : public true_type { }; /*template<class _Tp> struct is_array<_Tp[]>: public true_type { }; */ 

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.

Bild 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 { }; } // is_pointer template<class _Tp> struct is_pointer : public detail::_is_pointer_helper<typename remove_cv<_Tp>::type>::type { }; // is_lvalue_reference template<class> struct is_lvalue_reference : public false_type { }; template<class _Tp> struct is_lvalue_reference<_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); //  'func_ptr'    'void func(int){}' void (ClassType::*mem_func_ptr)(int); //  'mem_func_ptr'  -  'ClassType'  'void ClassType::func(int){}' 

  • Sie können einen Link zum ersten (Objektlink) erstellen, aber keinen zweiten Link.

 void (&func_ref)(int); //  'func_ref'    'void func(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 { // SFINAE magic typedef char _yes_type; struct _no_type { char padding[8]; }; } 

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 //      define  : #undef _IS_MEM_FUN_PTR #undef _IS_MEM_FUN_PTR_CLR #undef _IS_MEM_FUN_CDECL_PTR #undef _IS_MEM_FUN_STDCALL_PTR #undef _IS_MEM_FUN_FASTCALL_PTR } 

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".

 // is_function template<class _Tp> struct is_function { static const bool value = detail::_is_function_chooser<_Tp, is_reference<_Tp>::value>::value; typedef const bool value_type; typedef integral_constant<bool, is_function::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; 

Und für die Implementierung von is_member_function_pointer ist es noch einfacher:

 // is_member_function_pointer template<class _Tp> struct is_member_function_pointer : public detail::_is_mem_function_ptr_helper<typename remove_cv<_Tp>::type>::type { }; 

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 {}; } // is_member_object_pointer template<class _Tp> struct is_member_object_pointer : public detail::_is_member_object_pointer_helper<typename remove_cv<_Tp>::type>::type { }; 

Verwendete logische Operationen 'und', 'oder', 'nicht' für Typen aus dem ersten Teil
 namespace detail { struct void_type {}; //typedef void void_type; template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type> struct _or_ : public conditional<_B1::value, _B1, _or_<_B2, _or_<_B3, _B4> > >::type { }; template<> struct _or_<void_type, void_type, void_type, void_type>; template<class _B1> struct _or_<_B1, void_type, void_type, void_type> : public _B1 { }; template<class _B1, class _B2> struct _or_<_B1, _B2, void_type, void_type> : public conditional<_B1::value, _B1, _B2>::type { }; template<class _B1, class _B2, class _B3> struct _or_<_B1, _B2, _B3, void_type> : public conditional<_B1::value, _B1, _or_<_B2, _B3> >::type { }; template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type> struct _and_; template<> struct _and_<void_type, void_type, void_type, void_type>; template<class _B1> struct _and_<_B1, void_type, void_type, void_type> : public _B1 { }; template<class _B1, class _B2> struct _and_<_B1, _B2, void_type, void_type> : public conditional<_B1::value, _B2, _B1>::type { }; template<class _B1, class _B2, class _B3> struct _and_<_B1, _B2, _B3, void_type> : public conditional<_B1::value, _and_<_B2, _B3>, _B1>::type { }; template<class _Pp> struct _not_ { static const bool value = !bool(_Pp::value); typedef const bool value_type; typedef integral_constant<bool, _not_::value == bool(true)> type; operator value_type() const { // return stored value return (value); } value_type operator()() const { // return stored value return (value); } }; } 


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?):

 // is_arithmetic template<class _Tp> struct is_arithmetic : public detail::_or_<is_integral<_Tp>, is_floating_point<_Tp> >::type { }; // is_fundamental template<class _Tp> struct is_fundamental : public detail::_or_<is_arithmetic<_Tp>, is_void<_Tp>, is_null_pointer<_Tp> >::type {}; // is_object template<class _Tp> struct is_object : public detail::_not_< detail::_or_< is_function<_Tp>, is_reference<_Tp>, is_void<_Tp> > >::type {}; // is_scalar template<class _Tp> struct is_scalar : public detail::_or_<is_arithmetic<_Tp>, is_pointer<_Tp>, is_member_pointer<_Tp>, is_null_pointer<_Tp>/*, is_enum<_Tp>*/ >::type {}; // is_compound template<class _Tp> struct is_compound: public detail::_not_<is_fundamental<_Tp> >::type { }; 
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)) //&& !is_union<_Tp>::value >::value && (is_array<_Tp>::value == bool(false)) && (is_void<_Tp>::value == bool(false)) && (is_function<_Tp>::value == bool(false))> type; }; } // is_class template<class _Tp> struct is_class : public detail::_is_class_helper<typename remove_cv<_Tp>::type, is_reference<_Tp>::value>::type { }; 

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.

Source: https://habr.com/ru/post/de418347/


All Articles