How I wrote the standard C ++ 11 library or why the boost is so scary. Chapter 3

Published on July 16, 2018

How I wrote the standard C ++ 11 library or why the boost is so scary. Chapter 3

    We continue the adventure.

    Summary of the previous parts


    Due to limitations on the ability to use C ++ 11 compilers and from the lack of alternatives to boost, there was a desire to write your own implementation of the standard C ++ 11 library on top of the C ++ 98 / C ++ 03 library supplied with the compiler. The static_assert , noexcept , countof , and also after consideration

    were implemented of all non-standard defines and features of compilers, there is information about the functionality supported by the current compiler. This is where core.h is almost complete, but it would not be complete without nullptr . Link to GitHub with the result for today for impatient and non-readers:



    Commits and constructive criticism are welcome

    So let's continue.

    Table of contents


    Introduction
    Chapter 1. Viam supervadet vadens
    Chapter 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
    Chapter 3. Finding the perfect realization nullptr
    Chapter 4. Patterned "magic» the C ++
    .... 4.1 start small
    .... 4.2 About how many mistakes we Wonderful compilations are prepared by the log
    .... 4.3 Pointers and all-all-all
    .... 4.4 What else is needed for the sample library
    Chapter 5.
    ...

    Chapter 3. Finding the perfect implementation of nullptr


    After the whole epic with nonstandard compiler macros and discoveries of the “wonderful” they presented, I could finally add nullptr and it even warmed my soul. Finally, it will be possible to get rid of all these comparisons with 0 or even with NULL .

    imageMost programmers implement nullptr as
    #define nullptr 0
    

    and this could be the end of this chapter. If you want nullptr for yourself , then just replace 0 with such a define, because in fact this is all that is required for correct operation.

    Do not forget to really write a check, and then suddenly someone else will be with the following definition:

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

    The #error preprocessor directive will generate an error with human-readable text when compiled, and yes, this is a standard directive, the use of which is rare but can be found.

    But in this implementation, we miss one of the important points described in the standard, namely std :: nullptr_t - a separate type, of which nullptr is a constant instance . And the developers of chromium, when they also tried to solve this problem (now there is a newer compiler and a normal nullptr ), defining it as a class that can convert to a pointer to any type. Since according to the standard, the size of nullptr should be equal to the size of the pointer to void (and void *must also contain any pointer, except for pointers to a member of the class), a little “standardize” this implementation by adding an unused null pointer:

    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 T C::*() { 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)
    

    The conversion of this class to any pointer is due to a generic type operator, which is called if something is compared with nullptr . Ie expression char * my_pointer; if (my_pointer == nullptr) will actually be converted to if (my_pointer == nullptr.operator char * ()) , which will compare the pointer with 0. The second type operator is needed to convert nullptr to pointers to class members. And here Borland C ++ Builder 6.0 “distinguished itself”, who unexpectedly decided that these two operators are identical and he can easily compare pointers to a member of a class and ordinary pointers to each other, therefore uncertainty arises every time such a nullptrit is compared with a pointer (this is a bug, and maybe it is not only with this compiler). We write a separate implementation for this case:

    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)
    

    The advantages of this representation of nullptr are that now there is a separate type for std :: nullptr_t . Disadvantages? The nullptr is lost during compilation and comparison through a ternary operator; the compiler cannot resolve it.

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

    And I want "and checkered and go." Only one thing comes to mind: enum . The members of an enumeration in C ++ will have their own separate type, and will also be converted to int without problems (and in fact are integer constants). This property of the enumeration member will help us, because the very “special” 0, which is used instead of nullptr for pointers, is the most common int . I haven’t met such implementation of nullptr on the Internet, and maybe it’s also something bad, but I haven’t found any idea what. We write the implementation:

    #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)
    

    As you can see here, a bit more code than just the enum nullptr_t declaration with the member nullptr = 0 . First, the definition of NULL may not be. It should be defined in a fairly substantial list of standard headers , but as practice has shown, it is better to be safe and check for the presence of this macro. Secondly, enum representation in C ++ according to the implementation-defined standard, i.e. the type of an enumeration can be represented by any integer types (with the proviso that these types cannot be greater than int , if only enum values fit into it). For example, if you declare enum test {_1, _2}the compiler can easily represent it as short and then it is quite possible that sizeof ( test ) ! = sizeof (void *) . In order for the implementation of nullptr to meet the standard, you need to make sure that the size of the type that the compiler selects for nullptr_t_as_enum will match the size of the pointer, i.e. essentially equal to sizeof (void *) . To do this, use the nullptr_t_as templates ... select an integer type that will be equal to the pointer size, and then set the maximum element value in our enumeration to the maximum value of this integer type.
    I want to draw attention to the macro CHAR_BIT defined in the standard climits header . This macro is set to the number of bits in one char , i.e. the number of bits per byte on the current platform. A useful standard definition, which is undeservedly bypassed by developers sticking eights everywhere, although here and there in one byte is not at all 8 bits .

    And another feature is the assignment of NULL as the value of the element enum . Some compilers provide warning (and their concern can be understood) about the fact that NULL is assigned to a “non-index”. We take out the standard namespace to our local ptrdiff_detail so as not to clutter up the rest of the namespace with them, and further, to reassure the compiler, we explicitly convert NULL to std :: ptrdiff_t - another for some reason little-used type in C ++, which serves to represent the result of arithmetic operations (subtraction) with pointers and is usually an alias of type std :: size_t ( std :: intptr_t in C ++ 11).

    SFINAE


    Here, for the first time in my narration, we encounter the phenomenon of C ++ as substitution failure is not an error (SFINAE) . Briefly, the essence of it is that when the compiler enumerates the appropriate function overloads for a particular call, it should check them all, and not stop after the first failure or after the first suitable overload was found. Hence, his ambiguity messages appear when there are two overloads of the called function from the compiler's point of view, and also the ability of the compiler to select the most appropriate function overload for a particular call with specific parameters. This feature of the compiler allows you to do the lion's share of all the template "magic" (by the way hello std :: enable_if), as well as being the basis of both the boost and my library.

    Since, as a result, we have several implementations of nullptr, we use SFINAE to “select” the best one at the compilation stage. Let's declare the types “yes” and “no” for checking through the sizeof probe functions declared below.

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

    Here we will use the same principle as in the second chapter with countof and its definition through the sizeof the return value (array of elements) of the template function 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));
    };
    

    What is going on here? First, the compiler “ iterates ” overloading the function _is_convertable_to_void_ptr_tester with an argument of type T and a value of NULL (the value does not play a role, just NULL must be reducible to type T ). There are only two overloads - with the void * type and with a variable argument list (...) . Substituting the argument into each of these overloads, the compiler will choose the first one if the type is cast to a pointer to void , and the second if the cast cannot be performed. For the overload selected by the compiler, we will determine by sizeof the size of the value returned by the function, and since they are guaranteed to be different (sizeof ( _no_type ) == 8 , sizeof ( _yes_type ) == 1 ), then we can determine by size what overload the compiler has picked up and consequently whether our type is converted to void * or not.

    We will continue to use the same programming pattern in order to determine whether the object of the chosen type is converted to the representation nullptr_t into any pointer (essentially (T) ( STDEX_NULL ) and there is a future definition for 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)
            );
    };
    

    Of course, it is not possible to sort through all imaginable and inconceivable pointers and their combinations with the volatile and const modifiers , so I limited myself to these 9 checks (two for class function pointers, one for void pointer , seven for different type pointers), which is quite enough.

    As mentioned above, some (* cough, cough * ... Borland Builder 6.0 ... * cough *) compilers do not distinguish between a pointer to the type and class member, because we write another check for supporting this event to then select the desired implementation nullptr_t through the class if needed.

    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;
    };
    

    And then it remains only to check the different implementations of nullptr_t and choose the appropriate compiler for the compiler.

    Select the implementation of 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;
    };
    


    First, we check for the ability to represent nullptr_t as a class, but since I did not find a universal compiler- independent solution of how to verify that an object of type can be a compile-time constant (by the way, I am open to suggestions for this, because it is quite possible that this is possible), This option is always checked ( _can_be_ct_constant is always false ). Next, we switch to checking the version with the view through enum . If it was not possible to submit this way either (the compiler cannot provide a pointer or enum for some reason), then we try to present it as an integer type (whose size will be equal to the size of the pointer to void). Well, if it didn’t work either, then select the implementation of the type nullptr_t through void * .

    This place reveals much of the power of SFINAE in combination with C ++ templates, due to which it is possible to choose the necessary implementation without resorting to compiler-dependent macros, and indeed to macros (unlike the boost where all this would be stuffed with #ifdef #else checks endif ).

    It remains only to define the type alias for nullptr_t in the namespace stdex and define for nullptr (in order to meet another requirement of the standard that the address nullptr cannot be taken, as well as to be able to use nullptr as a compile time constant).

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

    The end of the third chapter. In the fourth chapter, I finally get to type_traits and what other bugs in the compilers I came across while developing.

    Thank you for attention.