Как я стандартную библиотеку C++11 писал или почему boost такой страшный. Глава 3

Продолжаем приключения.

Краткое содержание предыдущих частей


Из-за ограничений на возможность использовать компиляторы C++ 11 и от безальтернативности boost’у возникло желание написать свою реализацию стандартной библиотеки C++ 11 поверх поставляемой с компилятором библиотеки C++ 98 / C++ 03.

Были реализованны static_assert, noexcept, countof, а так же, после рассмотрения всех нестандартных дефайнов и особенностей компиляторов, появилась информация о функциональности, которая поддерживается текущим компилятором. На этом описание core.h почти закончено, но оно было бы не полным без nullptr.

Ссылка на GitHub с результатом на сегодня для нетерпеливых и нечитателей:

Коммиты и конструктивная критика приветствуются


Итак, продолжим.

Оглавление


Введение
Глава 1. Viam supervadet vadens
Глава 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Глава 3. Поиск идеальной реализации nullptr
Глава 4.

Глава 3. Поиск идеальной реализации nullptr


После всей эпопеи с нестандартными макросами компиляторов и открытий «чудных», которые они приподнесли, я наконец мог добавить nullptr и это как то даже грело душу. Наконец-то можно будет избавиться от всех этих сравнений с 0 или даже с NULL.

imageБольшинство программистов реализует nullptr как

#define nullptr 0


и на этом можно было бы и закончить данную главу. Если вам хочется себе nullptr, то просто замените 0 на такой дефайн, ведь по-сути это все что требуется для корректной работы.

Не забудьте правда написать проверку, а то вдруг кто-то еще найдется с таким определением:

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


Директива препроцессора #error выдаст ошибку с человекочитаемым текстом при компиляции, и, да, это стандартная директива, применение которой редко, но можно найти.

Но в такой реализации мы упускаем один из важных моментов, описанных в стандарте, а именно std: nullptr_t — отдельный тип, константным экземпляром которого является nullptr. И разработчики chromium когда то тоже пытались решить эту проблему (сейчас там уже компилятор новее и нормальный nullptr) определяя его как класс, который умеет преобразовываться к указателю на любой тип. Так как по стандарту размер nullptr должен быть равен размеру указателя, а void* должен так же вмещать в себя любой указатель, немного «стандартизируем» эту реализацию добавив неиспользуемый пустой указатель:

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 operator T*() const { return 0; }
        // Make nullptr convertible to any member pointer type.
        template 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)


Преобразование этого класса в любой указатель происходит за счет шаблонного оператора типа, который вызывается в том случае если что-то сравнивается с nullptr. Тоесть выражение char *my_pointer; if (my_pointer == nullptr) фактически будет преобразовано к if (my_pointer == nullptr.operator char*()), что сравнит указатель с 0. Второй оператор типа нужен для преобразования nullptr к указателям на члены класса. И здесь уже отличился Borland C++ Builder 6.0, который неожиданно решил, что у него эти два оператора выдают один и тот же результат, потому возникает неопределенность каждый раз, как только такой nullptr сравнивается с указателем. Пишем отдельную реализацию для такого случая:

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


Преимущества данного представления nullptr в том что теперь есть отдельный тип для std: nullptr_t. Недостатки? Теряется константность nullptr на время компиляции и сравнения через тернарный оператор компилятор разрешить не сможет.

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


А хочется «и шашечки и ехать». Решение приходит в голову только одно: enum. Члены перечисления в C++ будут иметь свой отдельный тип, а так же без проблем преобразуются к int (а по сути являются целочисленными константами). Такой реализации nullptr на просторах интернетов я не встречал, и, возможно, она тоже чем-то плоха, но у меня не нашлось идей чем. Напишем реализацию:

#ifdef NULL
    #define STDEX_NULL NULL
#else
    #define STDEX_NULL 0
#endif

namespace ptrdiff_detail
{
    using namespace std;
}

template
struct nullptr_t_as_ulong_type { typedef unsigned long type; };
template<>
struct nullptr_t_as_ulong_type { typedef unsigned long type; };
template
struct nullptr_t_as_ushort_type { typedef unsigned short type; };
template<>
struct nullptr_t_as_ushort_type { typedef nullptr_t_as_long_type::type type; };
template
struct nullptr_t_as_uint_type { typedef unsigned int type; };
template<>
struct nullptr_t_as_uint_type { typedef nullptr_t_as_short_type::type type; };

typedef nullptr_t_as_uint_type::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)


Как видно здесь немного больше кода чем просто объявление enum nullptr_t с членом nullptr = 0. Во-первых определения NULL может не быть. Он должен быть определен в довольно солидном списке стандартных заголовков, но как показала практика здесь лучше перестраховаться и проверить на наличие этого макроса. Во-вторых представление enum в C++ согласно стандарту implementation-defined, т.е. тип перечисления может быть представлен какими угодно целочисленными типами (с оговоркой что эти типы не могут быть больше чем int, если только значения enum «влезают» в него). К примеру если объявить enum test{_1, _2} компилятор легко может представить его как short и тогда вполне возможно что sizeof (test) != sizeof (void*). Чтобы реализация nullptr соотвествовала стандарту нужно убедиться что размер типа который выберет компилятор для nullptr_t_as_enum будет соотвествовать размеру указателя, т.е. по сути равняться sizeof (void*). Для этого с помощью шаблонов nullptr_t_as… подбираем такой целочисленный тип, который будет равняться размеру указателя, а затем выставляем максимальное значение элемента в нашем перечислении в максимальное значение этого целочисленного типа.

Хочу обратить внимание на макрос CHAR_BIT определенный в стандартном заголовке climits. Этот макрос выставляется в значение количества бит в одном char, т.е. количество бит в байте на текущей платформе. Полезное стандартное определение, которое незаслуженно обходят стороной разработчики втыкая везде восьмерки.


И еще одна особенность это присвоение NULL как значения элемента enum. Некоторые компиляторы дают warning (и их обеспокоенность можно понять) по поводу того, что NULL присваивается «неуказателю». Выносим стандартный namespace в свой локальный, чтобы не захламлять им все остальное пространство имен, и чтобы успокоить компилятор явно преобразуем NULL к std: ptrdiff_t — еще одному почему-то малоиспользуемому типу в C++, который представляет собой результат арифметических действий (вычитания) с указателями и обычно является псевдонимом типа std: size_t (std: intptr_t в C++ 11).

SFINAE


Здесь, впервые в моем повествовании, мы сталкиваемся с таким явлением в C++ как substitution failure is not an error (SFINAE). Если вкратце то суть его в том, что когда компилятор «перебирает» подходящие перегрузки функций для конкретного вызова он должен проверить их все, а не останавливаться после первой неудачи или после первой найденной подходящей перегрузки. Отсюда появляется и его сообщения об ambiguity, когда существует две одинаковые с точки зрения компилятора перегрузки вызываемой функции, и способность компилятора подобрать самую точно подходящую функцию под конкретный вызов. Эта особенность работы компилятора позволяет делать львиную долю всей шаблонной «магии» (кстати привет std: enable_if), а так же является основой как boost, так и моей библиотеки.

Так как в результате у нас существует несколько реализаций nullptr мы с помощью SFINAE «подбираем» самую лучшую на этапе компиляции. Объявим типы «да» и «нет» для проверки через sizeof функций-пробников, объявленных ниже.

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
    _yes_type _is_convertable_to_ptr_tester(_Tp*);
    template
    _no_type _is_convertable_to_ptr_tester(...);
}


Здесь будем использовать тот же принцип что и во второй главе с countof и его определением через sizeof возвращаемого значения (массива элементов) шаблонной фукнции COUNTOF_REQUIRES_ARRAY_ARGUMENT.

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


Что же здесь происходит? Сначала компилятор «перебирает» перегрузки функции _is_convertable_to_void_ptr_tester с аргументом типа T и значением NULL (значение роли не играет, просто NULL должен быть приводимым к типу T). Перегрузок всего две — с типом void* и с variable argument list (…). Подставляя в каждую из этих перегрузок аргумент, компилятор выберет первую если тип приводится к указателю на void, и вторую если приведение не может быть выполнено. У выбранной компилятором перегрузки мы с помощью sizeof определим размер возвращаемого функцией значения, а так как они гарантированно разные (sizeof (_no_type) == 8, sizeof (_yes_type) == 1), то сможем определить по размеру какую перегрузку подобрал компилятор и следовательно преобразуется ли наш тип в void* или нет.

Этот же шаблон программирования будем применять и далее для того чтобы определить преобразуется ли объект выбранного нами типа для представления nullptr_t в любой указатель (по сути (T)(STDEX_NULL) и есть будущее определение для nullptr).

template
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
struct _is_convertable_to_any_ptr_impl_helper
{
    static const bool value = (sizeof(nullptr_detail::_is_convertable_to_ptr_tester((NullPtrType) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type));
};

template
struct _is_convertable_to_any_ptr_impl
{


    static const bool value = _is_convertable_to_any_ptr_impl_helper::value &&
                                _is_convertable_to_any_ptr_impl_helper::value &&
                                _is_convertable_to_any_ptr_impl_helper::value &&
                                _is_convertable_to_any_ptr_impl_helper::value &&
                                _is_convertable_to_any_ptr_impl_helper::value &&
                                _is_convertable_to_any_ptr_impl_helper::value &&
                                _is_convertable_to_any_ptr_impl_helper::value;
};

template
struct _is_convertable_to_ptr_impl
{
    static const bool value = (
        _is_convertable_to_void_ptr_impl::value == bool(true) && 
        _is_convertable_to_any_ptr_impl::value == bool(true) &&
        _is_convertable_to_member_function_ptr_impl::value == bool(true)
        );
};


Конечно не возможно перебирать все мыслимые и немыслимые указатели и их сочетания с модификаторами volatile и const, потому я ограничился только этими 9ю проверками (две на указатели функций класса, одна на указатель на void, семь на указатели на разные типы), чего вполне достаточно.

Как упоминалось выше некоторые (*кхе-кхе*…Borland Builder 6.0…*кхе*) компиляторы не различают указатели на тип и на член класса, потому напишем еще вспомогательную проверку на этот случай чтобы потом выбрать нужную реализацию nullptr_t через класс если понадобится.

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

template
struct _nullptr_t_as_class_chooser
{
    typedef nullptr_detail::nullptr_t_as_class_impl type;
};

template<>
struct _nullptr_t_as_class_chooser
{
    typedef nullptr_detail::nullptr_t_as_class_impl1 type;
};


И далее остается только проверить разные реализации nullptr_t и выбрать подходящую под собирающий компилятор.

Выбираем реализацию nullptr_t
template
struct _nullptr_choose_as_int
{
    typedef nullptr_detail::nullptr_t_as_int type;
};

template
struct _nullptr_choose_as_enum
{
    typedef nullptr_detail::nullptr_t_as_enum type;
};

template
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
{
    typedef nullptr_detail::nullptr_t_as_void type;
};

template<>
struct _nullptr_choose_as_enum
{
    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::value;
        static const bool _equal_void_ptr = _is_equal_size_to_void_ptr::value;
    };

    typedef _nullptr_choose_as_int::type type;
};

template<>
struct _nullptr_choose_as_class
{
    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::value;
        static const bool _equal_void_ptr = _is_equal_size_to_void_ptr::value;
        static const bool _can_be_ct_constant = true;//_nullptr_can_be_ct_constant_impl::value;
    };

    typedef _nullptr_choose_as_enum::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::value;
        static const bool _can_be_ct_constant = _nullptr_can_be_ct_constant_impl::value;
    };

    typedef _nullptr_choose_as_class::type type;
};



Сначала мы проверяем на возможность представить nullptr_t как класс, но так как универсального компиляторонезависимого решения как проверить что объект типа может быть константой времени компиляции я не нашел, этот вариант всегда отметается (_can_be_ct_constant всегда false). Далее переключаемся на проверку варианта с представлением через enum. Если и так представить не удалось (не может компилятор представить через enum указатель или размер почему то не тот), то пробуем представить в виде целочисленного типа (у которого размер будет равен размеру указателя на void). Ну уж если и это не сработало, то выбираем реализацию типа nullptr_t через void*.

В этом месте раскрывается большая часть мощи SFINAE в сочетании с шаблонами C++, за счет чего удается выбрать необходимую реализацию, не прибегая к компиляторозависимым макросам (в отличие от boost где все это было бы напичкано проверками #ifdef #else #endif).

Остается только определить псевдоним типа для nullptr_t в namespace stdex и дефайн для nullptr (дабы соблюсти еще одно требование стандарта о том что адрес nullptr брать нельзя, а так же чтобы можно было использовать nullptr как константу времени компиляции).

namespace stdex
{
    typedef detail::_nullptr_chooser::type nullptr_t;
}

#define nullptr (stdex::nullptr_t)(STDEX_NULL)


Конец третьей главы. В четвертой главе я наконец доберусь до type_traits и на какие еще баги в компиляторах я наткнулся при разработке.

Благодарю за внимание.

© Habrahabr.ru