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