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

в 6:07, , рубрики: boost, borland c++ builder, c++, c++03, c++11, c++98, gcc, loki, open source, QNX, sfinae, Visual Studio, велосипедостроение, грязные хаки, Компиляторы, кроссплатформенная разработка, макросы, ненормальное программирование, Разработка под Linux, Совершенный код, стандартная библиотека, шаблонное программирование

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

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

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

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

Настало время type_traits и всей этой «особой шаблонной магии». В предыдущих частях данной главы мы рассмотрели мою реализацию базовых шаблонов стандартной библиотеки и в данной части речь пойдет про комбинацию техники SFINAE с шаблонами и немного о кодогенерации.

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

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

Больше шаблонов C++ под катом.

Оглавление

Введение
Глава 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 Указатели и все-все-все
Глава 5.

Глава 4. Шаблонная «магия» C++. Продолжение

4.3 Указатели и все-все-все

На данном этапе мне оставалось только получить информацию о том является ли тип массивом для std::is_array и можно было приступать к шаблонам для указателей. Реализация так же была тривиальна, однако не без допущений.

// is_array
template<class>
struct is_array :
    public false_type { };

template<class _Tp, std::size_t _Size>
struct is_array<_Tp[_Size]> :
    public true_type { };

/*template<class _Tp>
struct is_array<_Tp[]>:
    public true_type { }; */

Простая специализация шаблона для массивов заданной длины «отлавливает» все типы массивов, однако проблема возникает с неполным типом T[] (массив без указания длины). Дело в том что данный тип не определяется некоторыми компиляторами (C++ Builder) при специализации шаблона, и универсальное решение здесь я пока что не нашел.

После того как библиотека была «научена» определять встроенные типы, выравнивание в памяти типов, работать с модификаторами типов и другим базовым вещам через шаблоны во время компиляции настало время указателей и ссылок.

image В языке C++ можно выделить две группы указателей — указатели на члены класса и указатели на остальные объекты. Почему именно такое разделение важно для дальнейшей реализации стандартной библиотеки? Дело в том что указатели на члены класса имеют существенное отличие от остальных указателей наличием this, т.е. указателя на объект этого класса. И по стандарту указатели на член класса имеют отдельный синтаксис для определения, являются отдельным типом и не могут быть представлены через обычный указатель. На практике это выражается в том что размер указателя на член класса обычно больше чем размер обычного указателя (который == sizeof(void*)), т.к. для реализации виртуальных функций-членов класса, а так же хранения указателя this компиляторы обычно реализуют указатели на член класса как структуру (почитать про виртуальные ф-ии и про структуру). То как представить указатели на члены класса оставим, согласно стандарту, на усмотрение компилятора, но об этом различии в размере и представлении будем помнить рассматривая дальнейший код.

Для определения обычного указателя на объект напишем простой шаблон is_pointer, а так же шаблон is_lvalue_reference для ссылок на объект (is_rvalue_reference отставим, т.к. до 11 стандарта как оператора &&, так и в целом move-семантики не существовало):

namespace detail
{
    template<class>
    struct _is_pointer_helper :
        public false_type { };

    template<class _Tp>
    struct _is_pointer_helper<_Tp*> :
        public true_type { };
}

// is_pointer
template<class _Tp>
struct is_pointer :
    public detail::_is_pointer_helper<typename remove_cv<_Tp>::type>::type
{ };

// is_lvalue_reference
template<class>
struct is_lvalue_reference :
    public false_type { };

template<class _Tp>
struct is_lvalue_reference<_Tp&> :
    public true_type { };

Здесь уже нет чего-то принципиально нового, все то же самое делалось и в предыдущих частях данной главы. Продолжим определения указателей на объекты — теперь рассмотрим указатели на функции.

Важно понимать что функция и функция-член класса это совершенно разные сущности согласно стандарту:

  • На первую указатель будет обычный (указатель на объект), на вторую же будет указатель на член класса.

void (*func_ptr)(int); // указатель 'func_ptr' на функцию вида 'void func(int){}'
void (ClassType::*mem_func_ptr)(int); // указатель 'mem_func_ptr' на функцию-член класса 'ClassType' вида 'void ClassType::func(int){}'

  • На первую можно создать ссылку (ссылка на объект), а на вторую ссылку создать нельзя.

void (&func_ref)(int); // ссылка 'func_ref' на функцию вида 'void func(int){}'
//-------------------- // ссылка на функцию-член класса не определена стандартом

Здесь как раз упомяну немного о кодогенерации. Так как до C++ 11 отсутствовали шаблоны с переменным числом параметров, все шаблоны где могло быть разное количество параметров определялись через специализации основного шаблона с каким ни будь большим числом параметров на входе и их инициализацией параметрами-пустышками по-умолчанию. То же самое применялось и к перегрузкам функций, т.к. макросов с переменным числом параметров тоже не было. Так как написание руками по 60-70 строк однотипных специализаций шаблона, перегрузок функций является довольно унылым и бесполезным занятием, а так же чревато тем, что можно допустить ошибку я написал простой генератор кода шаблонов и перегрузок функций для этих целей. Я решил ограничиться определением функций до 24 параметров и выглядит это в коде довольно громоздко, но просто и понятно:

namespace detail
{
    template <class R>
    struct _is_function_ptr_helper : false_type {};
    template <class R >
    struct _is_function_ptr_helper<R(*)()> : true_type {};
    template <class R >
    struct _is_function_ptr_helper<R(*)(...)> : true_type {};
    template <class R, class T0>
    struct _is_function_ptr_helper<R(*)(T0)> : true_type {};
    template <class R, class T0>
    struct _is_function_ptr_helper<R(*)(T0 ...)> : true_type {};

...

    template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24>
    struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24)> : true_type {};
    template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24>
    struct _is_function_ptr_helper<R(*)(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24 ...)> : true_type {};
}

Определим знакомые с предыдущей главы типы для техники SFINAE:

namespace detail
{
    // SFINAE magic

    typedef char _yes_type;
    struct _no_type
    {
        char padding[8];
    };
}

Еще немного макросов для удобства

namespace detail
{
    #define _IS_MEM_FUN_PTR_CLR 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS)); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...)); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) volatile); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS) const volatile); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) volatile); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(T::*const volatile*)(ARGS...) const volatile);

#ifdef _STDEX_CDECL
		_no_type _STDEX_CDECL _is_mem_function_ptr(...);

#define _IS_MEM_FUN_CDECL_PTR 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS)); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) volatile); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__cdecl T::*const volatile*)(ARGS) const volatile);

#define _IS_MEM_FUN_STDCALL_PTR 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS)); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) volatile); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__stdcall T::*const volatile*)(ARGS) const volatile);

#define _IS_MEM_FUN_FASTCALL_PTR 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS)); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) volatile); 
		template <class R, class T TYPES > 
		_yes_type _is_mem_function_ptr(R(__fastcall T::*const volatile*)(ARGS) const volatile);
#else
		_no_type _is_mem_function_ptr(...);
#define _IS_MEM_FUN_CDECL_PTR
#define _IS_MEM_FUN_STDCALL_PTR
#define _IS_MEM_FUN_FASTCALL_PTR
#endif

#define _IS_MEM_FUN_PTR 
		_IS_MEM_FUN_PTR_CLR 
		_IS_MEM_FUN_CDECL_PTR 
		_IS_MEM_FUN_STDCALL_PTR 
		_IS_MEM_FUN_FASTCALL_PTR
}

Макросы определены для того чтобы можно было относительно удобно переопределять TYPES и ARGS дефайны как список типов и параметров, затем подставляя макрос _IS_MEM_FUN_PTR генерировать препроцессором определения для всех возможных типов функций. Так же стоит обратить внимание на то, что для компиляторов компании Microsoft важны еще соглашения о вызовах (__fastcall, __stdcall и __cdecl), т.к. с разными соглашениями функции будут разными, хотя набор аргументов и возвращаемое значение у них одно и то же. В результате вся эта грандиозная конструкция макросов используется довольно компактно:

namespace detail
{
    #define TYPES
    #define ARGS
    _IS_MEM_FUN_PTR
#undef TYPES
#undef ARGS

    #define TYPES , class T0
    #define ARGS T0
    _IS_MEM_FUN_PTR
#undef TYPES
#undef ARGS
    
    #define TYPES , class T0, class T1
    #define ARGS T0, T1
    _IS_MEM_FUN_PTR
#undef TYPES
#undef ARGS

...

    #define TYPES , class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16, class T17, class T18, class T19, class T20, class T21, class T22, class T23, class T24
    #define ARGS T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24
    _IS_MEM_FUN_PTR
#undef TYPES
#undef ARGS

// не забудем убрать все лишние define за собой:
#undef _IS_MEM_FUN_PTR
#undef _IS_MEM_FUN_PTR_CLR 		
#undef _IS_MEM_FUN_CDECL_PTR
#undef _IS_MEM_FUN_STDCALL_PTR
#undef _IS_MEM_FUN_FASTCALL_PTR
}

А теперь то, ради чего все это было написано:

namespace detail
{
    template <class _Tp, bool _IsRef>
    struct _is_mem_function_ptr_impl
    {
        static _Tp *p;
        static const bool value = (sizeof(_is_mem_function_ptr(_is_mem_function_ptr_impl::p)) == sizeof(_yes_type));

        typedef typename integral_constant<bool, _is_mem_function_ptr_impl::value == bool(true)>::type type;
    };

    template <class _Tp>
    struct _is_mem_function_ptr_impl<_Tp, true>:
        public false_type
    {};

    template <class _Tp>
    struct _is_mem_function_ptr_helper:
        public _is_mem_function_ptr_impl<_Tp, is_reference<_Tp>::value>::type
    {};

    template <class _Tp, bool _IsMemberFunctionPtr>
    struct _is_function_chooser_impl :
        public false_type
    { };

    template <class _Tp>
    struct _is_function_chooser_impl<_Tp, false> :
        public _is_function_ptr_helper<_Tp*>
    { };

    template<class _Tp, bool _IsRef = true>
    struct _is_function_chooser :
        public false_type
    { };

    template <class _Tp>
    struct _is_function_chooser<_Tp, false>
    {

        static const bool value = _is_function_chooser_impl<_Tp, _is_mem_function_ptr_helper<_Tp>::value>::value;
    };
}

Для проверки является ли тип функцией-членом класса сначала проверяется не является ли тип ссылочным. Затем создается указатель такого типа и подставляется в функцию-пробник. Используя технику SFINAE силами компилятора подбирается необходимая из перегрузок функций-пробников для такого указателя и на основе результата сравнения с _yes_type формируется результат.

На основе проверки на функцию-член класса пишется проверка типа на его принадлежность типу функции. Проверяем не является ли тип ссылочным, если нет, то ищем подходящую специализацию шаблонных структур-пробников для указателя этого типа, которые будут true_type для любых указателей на функции до 24 параметров.

И теперь используем полученный результат для реализации is_function. Здесь, по той же причине что и в предыдущей части, я не смог отнаследовать эту структуру от integral_constant, так что поведение integral_constant «имитируется».

// is_function
template<class _Tp>
struct is_function
{
    static const bool value = detail::_is_function_chooser<_Tp, is_reference<_Tp>::value>::value;

    typedef const bool value_type;
    typedef integral_constant<bool, is_function::value == bool(true)> type;

    operator value_type() const
    {	// return stored value
        return (value);
    }

    value_type operator()() const
    {	// return stored value
        return (value);
    }
};

А для реализации is_member_function_pointer все еще проще:

// is_member_function_pointer
template<class _Tp>
struct is_member_function_pointer :
    public detail::_is_mem_function_ptr_helper<typename remove_cv<_Tp>::type>::type
{ };

Далее на основе этих шаблонов мы можем определить является ли тип впринципе членом класса:

namespace detail
{
    template<class _Tp>
    struct _is_member_object_pointer_impl1 :
        public _not_< _or_<_is_function_ptr_helper<_Tp>, _is_mem_function_ptr_helper<_Tp> > >::type
    { };

    template<class _Tp>
    struct _is_member_object_pointer_impl2 :
        public false_type { };

    template<class _Tp, class _Cp>
    struct _is_member_object_pointer_impl2<_Tp _Cp::*> :
        public true_type { };

    template<class _Tp>
    struct _is_member_object_pointer_helper:
        public _and_<_is_member_object_pointer_impl1<_Tp>, _is_member_object_pointer_impl2<_Tp> >::type
    {};

}
// is_member_object_pointer
template<class _Tp>
struct is_member_object_pointer :
    public detail::_is_member_object_pointer_helper<typename remove_cv<_Tp>::type>::type
{ };

Использованные 'и', 'или', 'не' логические операции над типами из первой части

namespace detail
{
    struct void_type {};

    //typedef void void_type;

    template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type>
    struct _or_ :
        public conditional<_B1::value, _B1, _or_<_B2, _or_<_B3, _B4> > >::type
    { };


    template<>
    struct _or_<void_type, void_type, void_type, void_type>;

    template<class _B1>
    struct _or_<_B1, void_type, void_type, void_type> :
        public _B1
    { };

    template<class _B1, class _B2>
    struct _or_<_B1, _B2, void_type, void_type> :
        public conditional<_B1::value, _B1, _B2>::type
    { };

    template<class _B1, class _B2, class _B3>
    struct _or_<_B1, _B2, _B3, void_type> :
        public conditional<_B1::value, _B1, _or_<_B2, _B3> >::type
    { };

    template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type>
    struct _and_;


    template<>
    struct _and_<void_type, void_type, void_type, void_type>;

    template<class _B1>
    struct _and_<_B1, void_type, void_type, void_type> :
        public _B1
    { };

    template<class _B1, class _B2>
    struct _and_<_B1, _B2, void_type, void_type> :
        public conditional<_B1::value, _B2, _B1>::type
    { };

    template<class _B1, class _B2, class _B3>
    struct _and_<_B1, _B2, _B3, void_type> :
        public conditional<_B1::value, _and_<_B2, _B3>, _B1>::type
    { };

    template<class _Pp>
    struct _not_
    {
        static const bool value = !bool(_Pp::value);

        typedef const bool value_type;
        typedef integral_constant<bool, _not_::value == bool(true)> type;

        operator value_type() const
        {	// return stored value
            return (value);
        }

        value_type operator()() const
        {	// return stored value
            return (value);
        }
    };
}

Здесь использованы логические операции над типами, которые с помощью шаблона conditional в итоге выбирают подходящий шаблонный тип. Шаблонное программирование во всей его красе, в итоге на этапе компиляции мы имеем уже информацию о том является ли тип членом класса. Довольно «зубодробительно», но зато как эффектно и эффективно!

Еще немного чистого шаблонного программирования на этих же логических элементах и у нас есть is_fundamental, is_compound и т.п. признаки (меня это приводит в восторг, а вас?):

// is_arithmetic
template<class _Tp>
struct is_arithmetic :
    public detail::_or_<is_integral<_Tp>, is_floating_point<_Tp> >::type
{ };

// is_fundamental
template<class _Tp>
struct is_fundamental :
    public detail::_or_<is_arithmetic<_Tp>, is_void<_Tp>, is_null_pointer<_Tp> >::type
{};

// is_object
template<class _Tp>
struct is_object :
    public detail::_not_< detail::_or_< is_function<_Tp>, is_reference<_Tp>, is_void<_Tp> > >::type
{};

// is_scalar
template<class _Tp>
struct is_scalar :
    public detail::_or_<is_arithmetic<_Tp>, is_pointer<_Tp>, is_member_pointer<_Tp>, is_null_pointer<_Tp>/*, is_enum<_Tp>*/ >::type
{};

// is_compound
template<class _Tp>
struct is_compound:
    public detail::_not_<is_fundamental<_Tp> >::type
{ };

Внимательный читатель заметит что определение is_enum закомментировано. Дело в том что способов отличить enum от других типов я не нашел, но считаю что это реализуемо без применения компиляторозависимых макросов. Возможно внимательный и сведущий читатель подскажет свой способ или ход мыслей на этот счет.

Для определения того факта что тип является классом теперь необходимо всего ничего:

namespace detail
{
    template <class _Tp, bool _IsReference>
    struct _is_class_helper
    {
        typedef integral_constant<bool, false> type;
    };

    template <class _Tp>
    struct _is_class_helper<_Tp, false>
    {
        typedef integral_constant<bool,
            (is_scalar<_Tp>::value == bool(false))
            //&& !is_union<_Tp>::value >::value
            && (is_array<_Tp>::value == bool(false))
            && (is_void<_Tp>::value == bool(false))
            && (is_function<_Tp>::value == bool(false))> type;
    };
}

// is_class
template<class _Tp>
struct is_class :
    public detail::_is_class_helper<typename remove_cv<_Tp>::type, is_reference<_Tp>::value>::type
{ };

И все бы хорошо, но union в C++ отличить от класса в общем случае не представляется возможным. Потому что они очень похожи по своим «внешним проявлениям», а отличия (к примеру невозможность наследования от union) проверить без ошибок компиляции у меня не получилось. Возможно кто-то подскажет хитрый маневр для определения union при компиляции, тогда is_class будет в точности соответствовать стандарту.

В заключительной части данной главы я расскажу о том как был реализован std::decay и std::common_type, а так же что еще предстоит добавить в type_traits.

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

Автор: oktonion

Источник


* - обязательные к заполнению поля