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

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) { }
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) { }
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;
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;
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.