Wie ich die Standard-C ++ 11-Bibliothek geschrieben habe oder warum der Boost so unheimlich ist. Kapitel 3

    Wir setzen das Abenteuer fort.

    Zusammenfassung der vorherigen Teile


    Aufgrund von Einschränkungen auf der Fähigkeit C ++ Compiler 11 und aus gibt es keine Alternative boost'u den Wunsch , eine eigene Implementierung von Standard - C ++ 11 Bibliotheken über mit dem Compiler C ++ Bibliotheken 98 / C ++ geliefert zu schreiben 03.

    Es wurde implementiert static_assert , noexcept , countof sowie nach Berücksichtigung Von allen nicht standardmäßigen Definitionen und Funktionen von Compilern gibt es Informationen zu den Funktionen, die vom aktuellen Compiler unterstützt werden. Hier ist core.h fast vollständig, aber ohne nullptr nicht vollständig .

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

    Commits und konstruktive Kritik sind willkommen

    Also machen wir weiter.

    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 Sie die perfekte Realisierung nullptr
    Kapitel 4. Gemusterte „magic» die C ++
    .... 4.1 klein anfangen
    .... 4.2 Über wie viele Fehler , die wir Wunderbare Zusammenstellungen werden vom Protokoll vorbereitet
    .... 4.3 Zeiger und Alles-Alles-Alles
    .... 4.4 Was wird noch benötigt für die Beispielbibliothek
    Kapitel 5.
    ...

    Kapitel 3. Die perfekte Implementierung von nullptr finden


    Nach dem ganzen Epos mit nicht standardgemäßen Compilermakros und Entdeckungen des "Wundersamen" konnte ich endlich nullptr hinzufügen und es erwärmte sogar meine Seele. Schließlich ist es möglich, all diese Vergleiche mit 0 oder sogar mit NULL loszuwerden .

    BildDie meisten Programmierer implementieren nullptr als
    #define nullptr 0

    und dies könnte das Ende dieses Kapitels sein. Wenn Sie nullptr für sich selbst haben möchten, ersetzen Sie einfach 0 durch eine solche Definition, denn dies ist alles, was für den korrekten Betrieb erforderlich ist.

    Vergessen Sie nicht, wirklich einen Scheck zu schreiben, und plötzlich wird jemand anderes mit der folgenden Definition sein:

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

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

    Bei dieser Implementierung fehlt jedoch ein wichtiger Punkt, der im Standard beschrieben wird, nämlich std :: nullptr_t - ein separater Typ, bei dem nullptr eine konstante Instanz ist . Und die Entwickler von Chrom, als sie auch versuchten , dieses Problem zu lösen (jetzt gibt es einen neueren Compiler und einen normalen Nullptr ), definieren sie es als eine Klasse, die in einen Zeiger auf einen beliebigen Typ konvertieren kann. Da gemäß dem Standard die Größe von nullptr gleich der Größe des Zeigers auf void sein sollte (und void *muss auch einen beliebigen Zeiger enthalten, mit Ausnahme von Zeigern auf ein Member der Klasse), ein wenig "standardisieren" Sie diese Implementierung, indem Sie einen nicht verwendeten Nullzeiger hinzufügen:

    classnullptr_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 { return0; }
            // Make nullptr convertible to any member pointer type.template<typename C, typename T> operator T C::*() { return0; }
            booloperator==(nullptr_t_as_class_impl) const { returntrue; }
            booloperator!=(nullptr_t_as_class_impl) const { returnfalse; }
        private:
            // Do not allow taking the address of nullptr.voidoperator&();
            void *_padding;
    };
        typedef nullptr_t_as_class_impl nullptr_t;
        #define nullptr nullptr_t(0)

    Die Konvertierung dieser Klasse in einen beliebigen Zeiger erfolgt aufgrund eines generischen Typoperators , der aufgerufen wird, wenn etwas mit nullptr verglichen wird . Dh 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 ist erforderlich, um nullptr in Zeiger in Klassenmitglieder umzuwandeln . Und hier zeichnete sich Borland C ++ Builder 6.0 aus, der unerwartet entschied, dass diese beiden Operatoren identisch sind und er leicht Zeiger auf ein Mitglied einer Klasse und gewöhnliche Zeiger miteinander vergleichen kann. Daher entsteht jedes Mal ein solcher NullptrEs wird mit einem Zeiger verglichen (dies ist ein Fehler und möglicherweise nicht nur mit diesem Compiler). Wir schreiben eine separate Implementierung für diesen Fall:

    classnullptr_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 { return0; }
        booloperator==(nullptr_t_as_class_impl1) const { returntrue; }
        booloperator!=(nullptr_t_as_class_impl1) const { returnfalse; }
    private:
        // Do not allow taking the address of nullptr.voidoperator&();
        void *_padding;
    };
        typedef nullptr_t_as_class_impl1 nullptr_t;
        #define nullptr nullptr_t(0)

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

    unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr; // ошибка компиляции, слева и справа от ':' совершенно разные типы
    STATIC_ASSERT(nullptr == nullptr && !(nullptr != nullptr), nullptr_should_be_equal_itself); // ошибка компиляции, nullptr не является константной времени компиляции

    Und ich will "und checkered und geht." Die Lösung fällt nur eins ein: enum . Die Mitglieder einer Aufzählung in C ++ haben einen eigenen separaten Typ und werden ohne Probleme in int konvertiert (und sind ganzzahlige Konstanten). Diese Eigenschaft des Aufzählungsmitglieds wird uns helfen, da die "spezielle" 0, die anstelle von nullptr für Zeiger verwendet wird, das häufigste int ist . Ich habe eine solche Implementierung von nullptr im Internet nicht getroffen, und vielleicht ist es auch etwas schlimmes, aber ich habe keine Ahnung, was Wir schreiben die Implementierung:

    #ifdef NULL#define STDEX_NULL NULL#else#define STDEX_NULL 0#endifnamespace ptrdiff_detail
    {
        usingnamespacestd;
    }
    template<bool>
    structnullptr_t_as_ulong_type {typedefunsignedlong type; };
    template<>
    structnullptr_t_as_ulong_type<false> {typedefunsignedlong type; };
    template<bool>
    structnullptr_t_as_ushort_type {typedefunsignedshort type; };
    template<>
    structnullptr_t_as_ushort_type<false> {typedef nullptr_t_as_long_type<sizeof(unsignedlong) == sizeof(void*)>::type type; };
    template<bool>
    structnullptr_t_as_uint_type {typedefunsignedint type; };
    template<>
    structnullptr_t_as_uint_type<false> {typedef nullptr_t_as_short_type<sizeof(unsignedshort) == sizeof(void*)>::type type; };
    typedef nullptr_t_as_uint_type<sizeof(unsignedint) == 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 die Aufzählung nullptr_t mit dem Member nullptr = 0 . Erstens kann die Definition von NULL nicht sein. Es sollte in einer ziemlich umfangreichen Liste von Standard-Headern definiert werden , aber wie die Praxis gezeigt hat, ist es besser, sicher zu sein und nach dem Vorhandensein dieses Makros zu suchen. Zweitens: Aufzählungsdarstellung in C ++ gemäß dem implementierungsdefinierten Standard, d. H. Der Typ einer Aufzählung kann durch einen beliebigen Integer-Typ dargestellt werden (mit der Maßgabe, dass diese Typen nicht größer als int sein können , wenn nur Enumerationswerte in diesen passen ). Wenn Sie beispielsweise den Aufzählungstest {_1, _2} deklarierenDer Compiler kann es leicht als kurz darstellen und dann ist es durchaus möglich, dass sizeof ( test ) ! = sizeof (void *) . Damit die Implementierung von nullptr dem Standard entspricht, müssen Sie sicherstellen, dass die Größe des Typs, den der Compiler für nullptr_t_as_enum auswählt, der Größe des Zeigers entspricht, d. H. im Wesentlichen gleich sizeof (void *) . Verwenden Sie dazu die Templates nullptr_t_as ... Wählen Sie einen Integer-Typ aus, der der Zeigergröße entspricht, und setzen Sie dann den maximalen Elementwert in unserer Enumeration auf den maximalen Wert dieses Integer-Typs.
    Ich möchte die Aufmerksamkeit auf die Makro ziehen CHAR_BIT in den Standard - Header definiert climits . Dieses Makro wird in einem auf den Wert der Anzahl von Bits gesetzt char , d.h. Die Anzahl der Bits pro Byte auf der aktuellen Plattform. Eine nützliche Standarddefinition , die von Entwicklern, die überall acht setzen, unverdient übergangen wird, obwohl hier und da in einem Byte nicht einmal 8 Bit enthalten sind .

    Und ein weiteres Merkmal dieser Zuordnung NULL als Wert des Elements einer Enumeration . Einige Compiler geben eine Warnung aus (und ihre Besorgnis ist verständlich), dass NULL einem "Nicht-Index" zugewiesen ist . Wir nehmen den Standard- Namensraum für unser lokales ptrdiff_detail heraus , um den Rest des Namensraums nicht mit ihnen zu überladen , und um den Compiler zu beruhigen, konvertieren wir NULL explizit in std :: ptrdiff_t - ein anderer aus irgendeinem Grund wenig benutzter Typ in C ++, der das Ergebnis arithmetischer Operationen darstellt (Subtraktion) mit Zeigern und ist normalerweise ein Alias ​​vom Typ std :: size_t ( std :: intptr_t in C ++ 11).

    SFINAE


    Zum ersten Mal in meiner Erzählung stoßen wir hier auf das Phänomen C ++, da Substitutionsfehler kein Fehler (SFINAE) sind . Kurz gesagt besteht das Wesentliche darin, dass der Compiler, wenn er die entsprechenden Funktionsüberladungen für einen bestimmten Aufruf auflistet, diese alle überprüfen sollte und nicht nach dem ersten Fehler oder nach dem Auffinden der ersten geeigneten Überladung aufhören sollte. Daher erscheinen seine Mehrdeutigkeitsnachrichten , wenn es aus Sicht des Compilers zwei Überladungen der aufgerufenen Funktion gibt, und auch die Fähigkeit des Compilers, die für einen bestimmten Aufruf am besten geeignete Funktionsüberladung mit bestimmten Parametern auszuwählen. Diese Funktion des Compilers ermöglicht es Ihnen, den Löwenanteil aller "magischen" Vorlagen (übrigens Hallo std :: enable_if) auszuführen) sowie die Basis sowohl für den Schub als auch für meine Bibliothek.

    Da wir daher mehrere Implementierungen von nullptr haben, verwenden wir SFINAE, um die beste für die Kompilierung auszuwählen. Lassen Sie uns die Typen mit „ja“ und „nein“ erklären, um die unten angegebenen Größen der Sondenfunktionen zu überprüfen .

    namespace nullptr_detail
    {
        typedefchar _yes_type;
        struct _no_type
        {char padding[8];
        };
        structdummy_class {};
        _yes_type _is_convertable_to_void_ptr_tester(void*);
        _no_type _is_convertable_to_void_ptr_tester(...);
        typedefvoid(nullptr_detail::dummy_class::*dummy_class_f)(int);
        typedefint(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 dasselbe Prinzip wie im zweiten Kapitel mit countof und seiner Definition durch die Größe des Rückgabewerts (Array von Elementen) der Template-Funktion COUNTOF_REQUIRES_ARRAY_ARGUMENT .

    template<classT>
    struct _is_convertable_to_void_ptr_impl
    {staticconstbool 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 Funktion _is_convertable_to_void_ptr_tester mit einem Argument vom Typ T und einem Wert von NULL (der Wert spielt keine Rolle, nur NULL muss auf den Typ T reduziert werden können ). Es gibt nur zwei Überladungen - mit dem Typ void * und mit einer variablen Argumentliste (...) . Durch Einsetzen des Arguments in jede dieser Überladungen wählt der Compiler den ersten, wenn der Typ in einen Zeiger auf void umgewandelt wird , und den zweiten, wenn der Cast nicht ausgeführt werden kann. Ein ausgewählter Compiler Überlastung wir verwenden sizeof definieren die Größe der Funktion des Rückgabewertes, und da sie verschiedene garantiert (sizeof ( _no_type ) == 8 , sizeof ( _yes_type ) == 1 ), dann können wir anhand der Größe bestimmen, welche Überladung der Compiler aufgenommen hat, und folglich, ob unser Typ in void * umgewandelt wird oder nicht.

    Wir werden weiterhin das gleiche Programmiermuster verwenden, um zu bestimmen, ob das Objekt des ausgewählten Typs in die Repräsentation nullptr_t in einen beliebigen Zeiger (im Wesentlichen (T) ( STDEX_NULL ) und eine zukünftige Definition für nullptr ) umgewandelt wird.

    template<classT>
    struct _is_convertable_to_member_function_ptr_impl
    {staticconstbool 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<classNullPtrType, classT>
    struct _is_convertable_to_any_ptr_impl_helper
    {staticconstbool value = (sizeof(nullptr_detail::_is_convertable_to_ptr_tester<T>((NullPtrType) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type));
    };
    template<classT>
    struct _is_convertable_to_any_ptr_impl
    {staticconstbool 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, constbool>::value &&
                                    _is_convertable_to_any_ptr_impl_helper<T, volatilefloat>::value &&
                                    _is_convertable_to_any_ptr_impl_helper<T, volatileconstdouble>::value &&
                                    _is_convertable_to_any_ptr_impl_helper<T, nullptr_detail::dummy_class>::value;
    };
    template<classT>
    struct _is_convertable_to_ptr_impl
    {staticconstbool 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 vorstellbaren und undenkbaren Zeiger und deren Kombinationen mit den Modifizierern volatile und const zu sortieren. Daher beschränkte ich mich auf diese 9 Überprüfungen (zwei für Klassenfunktionszeiger, einer für einen leeren Zeiger , sieben für verschiedene Typzeiger), was völlig ausreichend ist.

    Wie oben erwähnt, (hust * hust * ... Borland Builder 6.0 ... * hust *) einige Compiler zwischen einem Zeiger auf die Art und Klasse Mitglied unterscheiden nicht, weil wir einen weiteren Scheck für die Unterstützung dieses Ereignis schreiben die gewünschte Umsetzung wählen nullptr_t durch die Klasse wenn nötig

    struct _member_ptr_is_same_as_ptr
    {structtest {};
        typedefvoid(test::*member_ptr_type)(void);
        staticconstbool 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;
    };
    

    Dann müssen Sie nur noch die verschiedenen Implementierungen von nullptr_t überprüfen und den geeigneten Compiler für den Compiler auswählen.

    Wählen Sie die Implementierung von nullptr_t aus
    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>
    {structas_int
        {typedef nullptr_detail::nullptr_t_as_int nullptr_t_as_int;
            staticconstbool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_int>::value;
            staticconstbool _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>
    {structas_enum
        {typedef nullptr_detail::nullptr_t_as_enum nullptr_t_as_enum;
            staticconstbool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_enum>::value;
            staticconstbool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_enum>::value;
            staticconstbool _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
    {structas_class
        {typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type nullptr_t_as_class;
            staticconstbool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_class>::value;
            staticconstbool _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 keine universelle, vom Compiler unabhängige Lösung gefunden habe, wie überprüft werden kann, ob ein Objekt vom Typ eine Kompilierungszeitkonstante sein kann (ich bin übrigens offen für Vorschläge, da dies durchaus möglich ist). Diese Option ist immer aktiviert ( _can_be_ct_constant ist immer false ). Als nächstes wechseln wir zur Überprüfung der Version mit der Ansicht durch Aufzählung . Wenn es auch nicht möglich war, diesen Weg zu übergeben (der Compiler kann aus irgendeinem Grund keinen Zeiger oder kein Enum bereitstellen ), versuchen wir, ihn als Ganzzahltyp darzustellen (dessen Größe der Größe des zu ungültigen Zeigers entspricht). Wenn es auch nicht funktioniert hat, wählen Sie die Implementierung des Typs nullptr_t bis void * aus .

    Diese Stelle zeigt viel von der Leistungsfähigkeit von SFINAE in Kombination mit C ++ - Vorlagen, wodurch die erforderliche Implementierung ausgewählt werden kann, ohne auf vom Compiler abhängige Makros und sogar auf Makros zurückgreifen zu müssen (im Gegensatz zum Boost, bei dem dies alles mit #ifdef #else-Prüfungen gefüllt wäre endif ).

    Es muss nur noch der Typalias für nullptr_t im Namespace stdex und für nullptr definiert werden (um einer anderen Anforderung des Standards zu genügen, dass die Adresse nullptr nicht verwendet werden kann, und auch nullptr verwenden zu können als Kompilierzeitkonstante).

    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 schließlich zu type_traits und zu den anderen Fehlern in den Compilern, auf die ich bei der Entwicklung gestoßen bin.

    Danke für die Aufmerksamkeit.

    Jetzt auch beliebt: