Wie habe ich die Standard-C ++ 11-Bibliothek geschrieben oder warum ist Boost so beängstigend? Kapitel 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 . Damit ist die Beschreibung von core.h abgeschlossen, ohne nullptr wäre sie jedoch nicht vollständig.

Link zu GitHub mit dem Ergebnis für heute für ungeduldige und Nichtleser:

Engagements und konstruktive Kritik sind willkommen

Also, lass uns weitermachen.

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 3. Finden der perfekten nullptr-Implementierung


Nach dem ganzen Epos mit nicht standardmäßigen Compiler-Makros und den „wunderbaren“ Entdeckungen, die sie präsentierten, konnte ich endlich nullptr hinzufügen und es erwärmte meine Seele. Schließlich können Sie all diese Vergleiche mit 0 oder sogar NULL loswerden.

Bild Die meisten Programmierer implementieren nullptr als
#define nullptr 0 

und dies hätte dieses Kapitel beenden können. Wenn Sie sich nullptr wünschen , ersetzen Sie einfach 0 durch eine solche Definition, da dies im Wesentlichen alles ist, was für eine korrekte Operation erforderlich ist.

Vergessen Sie nicht, wirklich einen Scheck auszustellen, sonst wird plötzlich jemand anderes mit dieser Definition gefunden:

 #ifndef nullptr #define nullptr 0 #else #error "nullptr defined already" #endif 

Die Präprozessor-Direktive #error erzeugt beim Kompilieren einen Fehler mit lesbarem Text. Ja, dies ist eine Standard-Direktive, deren Verwendung selten ist, aber gefunden werden kann.

In einer solchen Implementierung fehlt jedoch einer der im Standard beschriebenen wichtigen Punkte, nämlich std :: nullptr_t - ein separater Typ, dessen konstante Instanz nullptr ist . Und Chromentwickler haben auch einmal versucht , dieses Problem zu lösen (jetzt gibt es einen neueren Compiler und normales Nullptr ) und es als eine Klasse definiert, die in einen Zeiger auf einen beliebigen Typ konvertiert werden kann. Da die Größe von nullptr standardmäßig der Größe des Zeigers auf void entsprechen sollte (und void * auch einen Zeiger enthalten sollte, mit Ausnahme von Zeigern auf ein Mitglied der Klasse), standardisieren wir diese Implementierung, indem wir einen nicht verwendeten Nullzeiger hinzufügen:

 class nullptr_t_as_class_impl { public: nullptr_t_as_class_impl() { } nullptr_t_as_class_impl(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } // Make nullptr convertible to any member pointer type. template<typename C, typename T> operator TC::*() { return 0; } bool operator==(nullptr_t_as_class_impl) const { return true; } bool operator!=(nullptr_t_as_class_impl) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl nullptr_t; #define nullptr nullptr_t(0) 

Die Konvertierung dieser Klasse in einen beliebigen Zeiger ist auf den Vorlagenoperator des Typs zurückzuführen, der aufgerufen wird, wenn etwas mit nullptr verglichen wird . Das heißt, der Ausdruck char * my_pointer; if (my_pointer == nullptr) wird tatsächlich in if (my_pointer == nullptr.operator char * ()) konvertiert, wodurch der Zeiger mit 0 verglichen wird. Der zweite Typoperator wird benötigt, um nullptr in Zeiger auf Klassenmitglieder zu konvertieren. Und hier hat sich Borland C ++ Builder 6.0 „ausgezeichnet“, der unerwartet entschieden hat, dass diese beiden Operatoren identisch sind und Zeiger mit einem Klassenmitglied und gewöhnlichen Zeigern leicht miteinander vergleichen können, sodass jedes Mal, wenn ein solcher Nullptr verglichen wird, eine Unsicherheit besteht Zeiger (dies ist ein Fehler, und vielleicht ist es nicht nur mit diesem Compiler). Wir schreiben eine separate Implementierung für diesen Fall:

 class nullptr_t_as_class_impl1 { public: nullptr_t_as_class_impl1() { } nullptr_t_as_class_impl1(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } bool operator==(nullptr_t_as_class_impl1) const { return true; } bool operator!=(nullptr_t_as_class_impl1) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl1 nullptr_t; #define nullptr nullptr_t(0) 

Die Vorteile dieser Nullptr- Ansicht sind, dass es jetzt einen separaten Typ für std :: nullptr_t gibt . Nachteile? Die Nullptr- Konstante geht beim Kompilieren und Vergleichen durch den ternären Operator verloren. Der Compiler kann sie nicht auflösen.

 unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr; //  ,     ':'    STATIC_ASSERT(nullptr == nullptr && !(nullptr != nullptr), nullptr_should_be_equal_itself); //  , nullptr      

Und ich möchte "und Dame und gehen." Die Lösung fällt nur einem ein: Aufzählung . Die Mitglieder der Aufzählung in C ++ haben einen eigenen Typ und werden problemlos in int konvertiert (und tatsächlich sind sie ganzzahlige Konstanten). Diese Eigenschaft eines Aufzählungselements hilft uns, da die sehr spezielle 0, die anstelle von nullptr für Zeiger verwendet wird, die häufigste int ist . Ich habe eine solche Implementierung von nullptr im Internet nicht gesehen, und vielleicht ist es auch etwas Schlechtes, aber ich hatte keine Ahnung warum. Schreiben wir eine Implementierung:

 #ifdef NULL #define STDEX_NULL NULL #else #define STDEX_NULL 0 #endif namespace ptrdiff_detail { using namespace std; } template<bool> struct nullptr_t_as_ulong_type { typedef unsigned long type; }; template<> struct nullptr_t_as_ulong_type<false> { typedef unsigned long type; }; template<bool> struct nullptr_t_as_ushort_type { typedef unsigned short type; }; template<> struct nullptr_t_as_ushort_type<false> { typedef nullptr_t_as_long_type<sizeof(unsigned long) == sizeof(void*)>::type type; }; template<bool> struct nullptr_t_as_uint_type { typedef unsigned int type; }; template<> struct nullptr_t_as_uint_type<false> { typedef nullptr_t_as_short_type<sizeof(unsigned short) == sizeof(void*)>::type type; }; typedef nullptr_t_as_uint_type<sizeof(unsigned int) == sizeof(void*)>::type nullptr_t_as_uint; enum nullptr_t_as_enum { _nullptr_val = ptrdiff_detail::ptrdiff_t(STDEX_NULL), _max_nullptr = nullptr_t_as_uint(1) << (CHAR_BIT * sizeof(void*) - 1) }; typedef nullptr_t_as_enum nullptr_t; #define nullptr nullptr_t(STDEX_NULL) 

Wie Sie hier sehen können, ist etwas mehr Code als nur das Deklarieren von enum nullptr_t mit dem Mitglied nullptr = 0 . Erstens gibt es möglicherweise keine NULL- Definitionen. Es sollte in einer ziemlich soliden Liste von Standard-Headern definiert werden , aber wie die Praxis gezeigt hat, ist es besser, auf Nummer sicher zu gehen und nach diesem Makro zu suchen. Zweitens ist die Aufzählungsdarstellung in C ++ gemäß dem implementierungsdefinierten Standard, d.h. Der Aufzählungstyp kann durch beliebige Ganzzahltypen dargestellt werden (mit der Maßgabe, dass diese Typen nicht größer als int sein dürfen , sofern die Aufzählungswerte in ihn passen ). Wenn Sie beispielsweise den Aufzählungstest {_1, _2} deklarieren , kann der Compiler ihn leicht als kurz darstellen, und dann ist es durchaus möglich, dass sizeof ( test ) ! = Sizeof (void *) . Damit die nullptr- Implementierung dem Standard entspricht, müssen Sie sicherstellen, dass die Größe des Typs, den der Compiler für nullptr_t_as_enum auswählt, mit der Größe des Zeigers übereinstimmt , d. H. im wesentlichen gleich groß von (void *) . Wählen Sie dazu mithilfe der Vorlagen nullptr_t_as ... einen Ganzzahltyp aus, der der Größe des Zeigers entspricht, und setzen Sie dann den Maximalwert des Elements in unserer Aufzählung auf den Maximalwert dieses Ganzzahltyps.
Ich möchte auf das Makro CHAR_BIT achten, das im Standard- Klimaheader definiert ist. Dieses Makro wird auf die Anzahl der Bits in einem Zeichen gesetzt, d.h. Die Anzahl der Bits pro Byte auf der aktuellen Plattform. Eine nützliche Standarddefinition , die Entwickler unverdient umgehen, indem sie überall acht halten, obwohl an einigen Stellen in einem Byte überhaupt keine 8 Bits vorhanden sind .

Ein weiteres Merkmal ist die Zuweisung von NULL als Wert des enum- Elements. Einige Compiler geben eine Warnung (und ihre Besorgnis kann verstanden werden) darüber aus, dass NULL dem "Nicht-Indexer" zugewiesen ist . Wir entfernen den Standard- Namespace in unser lokales ptrdiff_detail , um den Rest des Namespace nicht zu überladen , und konvertieren dann zur Beruhigung des Compilers explizit NULL in std :: ptrdiff_t - einen anderen irgendwie nicht ausreichend genutzten Typ in C ++, der dazu dient, das Ergebnis arithmetischer Operationen darzustellen (Subtraktion) mit Zeigern und ist normalerweise ein Alias ​​vom Typ std :: size_t ( std :: intptr_t in C ++ 11).

SFINAE


Hier sind wir zum ersten Mal in meiner Geschichte mit einem solchen Phänomen in C ++ konfrontiert, da Substitutionsfehler kein Fehler sind (SFINAE) . Kurz gesagt, das Wesentliche dabei ist, dass der Compiler, wenn er die entsprechenden Funktionsüberladungen für einen bestimmten Aufruf „durchläuft“, alle überprüft und nicht nach dem ersten Fehler oder nach der ersten geeigneten Überladung stoppt. Von hier kommt seine Botschaft über Mehrdeutigkeit , wenn es zwei Überladungen der aufgerufenen Funktion gibt, die aus Sicht des Compilers identisch sind, sowie die Fähigkeit des Compilers, die am besten geeignete Funktionsüberladung für einen bestimmten Aufruf mit bestimmten Parametern auszuwählen. Diese Funktion des Compilers ermöglicht es Ihnen, den Löwenanteil der gesamten Vorlage "magic" (übrigens hi std :: enable_if ) zu erledigen , und ist auch die Grundlage für Boost und meine Bibliothek.

Da wir daher mehrere nullptr- Implementierungen haben , verwenden wir SFINAE, um die besten in der Kompilierungsphase auszuwählen. Wir deklarieren die Typen "Ja" und "Nein", um die Größe der unten deklarierten Sondenfunktionen zu überprüfen.

 namespace nullptr_detail { typedef char _yes_type; struct _no_type { char padding[8]; }; struct dummy_class {}; _yes_type _is_convertable_to_void_ptr_tester(void*); _no_type _is_convertable_to_void_ptr_tester(...); typedef void(nullptr_detail::dummy_class::*dummy_class_f)(int); typedef int (nullptr_detail::dummy_class::*dummy_class_f_const)(double&) const; _yes_type _is_convertable_to_member_function_ptr_tester(dummy_class_f); _no_type _is_convertable_to_member_function_ptr_tester(...); _yes_type _is_convertable_to_const_member_function_ptr_tester(dummy_class_f_const); _no_type _is_convertable_to_const_member_function_ptr_tester(...); template<class _Tp> _yes_type _is_convertable_to_ptr_tester(_Tp*); template<class> _no_type _is_convertable_to_ptr_tester(...); } 

Hier verwenden wir das gleiche Prinzip wie im zweiten Kapitel mit countof und seiner Definition durch sizeof des Rückgabewerts (Array von Elementen) der Vorlagenfunktion COUNTOF_REQUIRES_ARRAY_ARGUMENT .

 template<class T> struct _is_convertable_to_void_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_void_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; 

Was ist hier los? Zunächst „ iteriert “ der Compiler die Überladungen der Funktion _is_convertable_to_void_ptr_tester mit einem Argument vom Typ T und dem Wert NULL (der Wert spielt keine Rolle, nur NULL muss vom Typ T sein ). Es gibt nur zwei Überladungen - mit dem Typ void * und mit der Liste der variablen Argumente (...) . Durch Einsetzen eines Arguments in jede dieser Überladungen wählt der Compiler das erste aus, wenn der Typ in einen Zeiger auf void umgewandelt wird , und das zweite, wenn die Umwandlung nicht ausgeführt werden kann. Mit der vom Compiler ausgewählten Überladung bestimmen wir mit sizeof die Größe des von der Funktion zurückgegebenen Werts. Da diese garantiert unterschiedlich sind ( sizeof ( _no_type ) == 8 , sizeof ( _yes_type ) == 1 ), können wir die Größe der Überladung bestimmen, die der Compiler aufgenommen und daher konvertiert hat ob unser Typ nichtig ist * oder nicht.

Wir werden dieselbe Programmiervorlage weiter anwenden, um zu bestimmen, ob ein Objekt des Typs unserer Wahl zur Darstellung von nullptr_t in einen Zeiger konvertiert wird (im Wesentlichen ist (T) ( STDEX_NULL ) die zukünftige Definition für nullptr ).

 template<class T> struct _is_convertable_to_member_function_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)) && (sizeof(nullptr_detail::_is_convertable_to_const_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class NullPtrType, class T> struct _is_convertable_to_any_ptr_impl_helper { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_ptr_tester<T>((NullPtrType) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class T> struct _is_convertable_to_any_ptr_impl { static const bool value = _is_convertable_to_any_ptr_impl_helper<T, int>::value && _is_convertable_to_any_ptr_impl_helper<T, float>::value && _is_convertable_to_any_ptr_impl_helper<T, bool>::value && _is_convertable_to_any_ptr_impl_helper<T, const bool>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile float>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile const double>::value && _is_convertable_to_any_ptr_impl_helper<T, nullptr_detail::dummy_class>::value; }; template<class T> struct _is_convertable_to_ptr_impl { static const bool value = ( _is_convertable_to_void_ptr_impl<T>::value == bool(true) && _is_convertable_to_any_ptr_impl<T>::value == bool(true) && _is_convertable_to_member_function_ptr_impl<T>::value == bool(true) ); }; 

Natürlich ist es nicht möglich, alle denkbaren und unvorstellbaren Zeiger und ihre Kombinationen mit flüchtigen und konstanten Modifikatoren zu durchlaufen, daher habe ich mich auf nur diese 9 Prüfungen beschränkt (zwei auf Zeiger auf Klassenfunktionen, eine auf Zeiger auf void , sieben auf Zeiger auf verschiedene Typen), was völlig ausreichend ist.

Wie oben erwähnt, unterscheiden einige (* khe-khe * ... Borland Builder 6.0 ... * khe *) Compiler nicht zwischen Zeigern auf einen Typ und ein Mitglied einer Klasse. Daher schreiben wir für diesen Fall eine weitere Hilfsprüfung , damit wir dann die gewünschte Implementierung von nullptr_t über die Klasse auswählen können wenn nötig.

 struct _member_ptr_is_same_as_ptr { struct test {}; typedef void(test::*member_ptr_type)(void); static const bool value = _is_convertable_to_void_ptr_impl<member_ptr_type>::value; }; template<bool> struct _nullptr_t_as_class_chooser { typedef nullptr_detail::nullptr_t_as_class_impl type; }; template<> struct _nullptr_t_as_class_chooser<false> { typedef nullptr_detail::nullptr_t_as_class_impl1 type; }; 

Und dann müssen nur noch die verschiedenen Implementierungen von nullptr_t überprüft und der entsprechende Compiler für den Compiler ausgewählt werden.

Auswahl der Implementierung nullptr_t
 template<bool> struct _nullptr_choose_as_int { typedef nullptr_detail::nullptr_t_as_int type; }; template<bool> struct _nullptr_choose_as_enum { typedef nullptr_detail::nullptr_t_as_enum type; }; template<bool> struct _nullptr_choose_as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type type; }; template<> struct _nullptr_choose_as_int<false> { typedef nullptr_detail::nullptr_t_as_void type; }; template<> struct _nullptr_choose_as_enum<false> { struct as_int { typedef nullptr_detail::nullptr_t_as_int nullptr_t_as_int; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_int>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_int>::value; }; typedef _nullptr_choose_as_int<as_int::_is_convertable_to_ptr == bool(true) && as_int::_equal_void_ptr == bool(true)>::type type; }; template<> struct _nullptr_choose_as_class<false> { struct as_enum { typedef nullptr_detail::nullptr_t_as_enum nullptr_t_as_enum; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_enum>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_enum>::value; static const bool _can_be_ct_constant = true;//_nullptr_can_be_ct_constant_impl<nullptr_t_as_enum>::value; }; typedef _nullptr_choose_as_enum<as_enum::_is_convertable_to_ptr == bool(true) && as_enum::_equal_void_ptr == bool(true) && as_enum::_can_be_ct_constant == bool(true)>::type type; }; struct _nullptr_chooser { struct as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type nullptr_t_as_class; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_class>::value; static const bool _can_be_ct_constant = _nullptr_can_be_ct_constant_impl<nullptr_t_as_class>::value; }; typedef _nullptr_choose_as_class<as_class::_equal_void_ptr == bool(true) && as_class::_can_be_ct_constant == bool(true)>::type type; }; 


Zuerst prüfen wir, ob nullptr_t als Klasse dargestellt werden kann. Da ich jedoch keinen universellen Compiler für eine unabhängige Lösung gefunden habe, habe ich kein Typobjekt gefunden , das eine Konstante der Kompilierungszeit darstellen kann (ich bin übrigens offen für Vorschläge zu diesem Thema, da dies wahrscheinlich möglich ist). Diese Option ist immer aktiviert ( _can_be_ct_constant ist immer falsch ). Als nächstes wechseln wir zur Überprüfung der Variante mit der Ansicht durch Aufzählung . Wenn es immer noch nicht möglich war zu präsentieren (der Compiler kann einen Zeiger nicht durch enum präsentieren oder die Größe ist irgendwie falsch), versuchen wir, ihn als einen ganzzahligen Typ darzustellen (dessen Größe der Größe des Zeigers auf void entspricht ). Nun, auch wenn dies nicht funktioniert hat, wählen wir eine Implementierung des Typs nullptr_t über void * aus .

An diesem Punkt wird der größte Teil der Leistung von SFINAE in Kombination mit C ++ - Vorlagen offenbart, wodurch es möglich ist, die erforderliche Implementierung auszuwählen, ohne auf compilerabhängige Makros und tatsächlich auf Makros zurückzugreifen (im Gegensatz zu Boost, bei dem all dies mit #ifdef #else # -Prüfungen überfüllt wäre endif ).

Es bleibt nur ein Typalias für nullptr_t im Namespace stdex und eine Definition für nullptr zu definieren (um einer anderen Standardanforderung zu entsprechen, dass die nullptr- Adresse nicht verwendet werden kann, sowie nullptr als Kompilierungszeitkonstante zu verwenden).

 namespace stdex { typedef detail::_nullptr_chooser::type nullptr_t; } #define nullptr (stdex::nullptr_t)(STDEX_NULL) 

Das Ende des dritten Kapitels. Im vierten Kapitel komme ich endlich zu type_traits und den anderen Fehlern in den Compilern, auf die ich während der Entwicklung gestoßen bin.

Danke für die Aufmerksamkeit.

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


All Articles