Pull to refresh

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

Reading time 11 min
Views 12K
Продолжаем приключения.

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


Из-за ограничений на возможность использовать компиляторы 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. Шаблонная «магия» C++
....4.1 Начинаем с малого
....4.2 О сколько нам ошибок чудных готовит компиляций лог
....4.3 Указатели и все-все-все
....4.4 Что же еще нужно для шаблонной библиотеки
Глава 5.


Глава 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 должен быть равен размеру указателя на voidvoid* должен так же вмещать в себя любой указатель, кроме указателей на член класса) немного «стандартизируем» эту реализацию добавив неиспользуемый пустой указатель:

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)

Преобразование этого класса в любой указатель происходит за счет шаблонного оператора типа, который вызывается в том случае если что-то сравнивается с 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<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)

Преимущества данного представления 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 (а по сути являются целочисленными константами). Такое свойство члена перечисления нам поможет, ведь тот самый «особенный» 0, который используется вместо nullptr для указателей и есть самый обычный int. Такой реализации nullptr на просторах интернетов я не встречал, и, возможно, она тоже чем-то плоха, но у меня не нашлось идей чем. Напишем реализацию:

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

Как видно здесь немного больше кода чем просто объявление 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, т.е. количество бит в байте на текущей платформе. Полезное стандартное определение, которое незаслуженно обходят стороной разработчики втыкая везде восьмерки, хотя кое-где в одном байте совсем не 8 бит.

И еще одна особенность это присвоение NULL как значения элемента enum. Некоторые компиляторы дают warning (и их обеспокоенность можно понять) по поводу того, что NULL присваивается «неуказателю». Выносим стандартный namespace в свой локальный ptrdiff_detail, чтобы не захламлять им все остальное пространство имен, и далее, чтобы успокоить компилятор, явно преобразуем 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<class _Tp>
    _yes_type _is_convertable_to_ptr_tester(_Tp*);
    template<class>
    _no_type _is_convertable_to_ptr_tester(...);
}

Здесь будем использовать тот же принцип что и во второй главе с countof и его определением через sizeof возвращаемого значения (массива элементов) шаблонной функции 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));
};

Что же здесь происходит? Сначала компилятор «перебирает» перегрузки функции _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<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)
        );
};

Конечно не возможно перебирать все мыслимые и немыслимые указатели и их сочетания с модификаторами 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<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;
};

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

Выбираем реализацию 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;
};


Сначала мы проверяем на возможность представить 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 и на какие еще баги в компиляторах я наткнулся при разработке.

Благодарю за внимание.
Tags:
Hubs:
+33
Comments 21
Comments Comments 21

Articles