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

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


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

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

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

Ссылка на 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 О сколько нам ошибок чудных готовит компиляций лог
Глава 5.


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


4.2 О сколько нам ошибок чудных готовит компиляций лог


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

К примеру просто необходимы были шаблоны is_integral и is_floating_point, которые определены на самом деле очень тривиально — через специализацию шаблона для каждого встроенного типа. Вопрос здесь только возникал с «большими» типами long long. Дело в том что данный тип как встроенный появляется в стандарте языка C++ только с 11 версии. И логично было бы предположить что все сводится к проверке версии стандарта C++ (которую и так то однозначно сложно определить), но не тут то было.

imageПотому как с 1999 года существует стандарт языка Си C99, в котором типы long long int и unsigned long long int уже присутствовали (с 1999 года!), и, так как язык C++ стремился сохранять обратную совместимость с чистым C, то многие компиляторы (которые обычно были смешанными C/C++) просто добавили его как фундаментальный тип еще до выхода даже стандарта C++03. То есть получалась ситуация что встроенный тип по факту есть (из Си), но в стандарте C++ он не описан и его там быть не должно. И это вносит еще немного путаницы в реализацию стандартной библиотеки. Но давайте посмотрим на код:

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

    template<> struct _is_floating_point<float> : public true_type {};
    template<> struct _is_floating_point<double> : public true_type {};
    template<> struct _is_floating_point<long double> : public true_type {};
}
template <class _Tp> 
struct is_floating_point : 
    public detail::_is_floating_point<typename remove_cv<_Tp>::type> 
{ };

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

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

    template<> struct _is_integral_impl<bool> : public true_type {};
    template<> struct _is_integral_impl<char> : public true_type {};
    template<> struct _is_integral_impl<wchar_t> : public true_type {};



    template<> struct _is_integral_impl<unsigned char> : public true_type {};
    template<> struct _is_integral_impl<unsigned short int> : public true_type {};
    template<> struct _is_integral_impl<unsigned int> : public true_type {};
    template<> struct _is_integral_impl<unsigned long int> : public true_type {};

    #ifdef LLONG_MAX
    template<> struct _is_integral_impl<unsigned long long int> : public true_type {};
    #endif

    template<> struct _is_integral_impl<signed char> : public true_type {};
    template<> struct _is_integral_impl<short int> : public true_type {};
    template<> struct _is_integral_impl<int> : public true_type {};
    template<> struct _is_integral_impl<long int> : public true_type {};

    #ifdef LLONG_MAX
    template<> struct _is_integral_impl<long long int> : public true_type {};
    #endif

    template <class _Tp> struct _is_integral : public _is_integral_impl<_Tp> {};

    template<> struct _is_integral<char16_t> : public true_type {};
    template<> struct _is_integral<char32_t> : public true_type {};

    template<> struct _is_integral<int64_t> : public true_type {};
    template<> struct _is_integral<uint64_t> : public true_type {};
}
template <class _Tp>
struct is_integral :
    public detail::_is_integral<typename remove_cv<_Tp>::type>
{ };

Здесь необходимо немного остановиться и подумать. Для «старых» целочисленных типов вроде int, bool и т.п. мы делаем такие же специализации как и в случае с is_floating_point. Для «новых» типов long long int и его беззнакового собрата мы определяем перегрузки только при наличии дефайна LLONG_MAX, который был определен в C++11 (как первом стандарте C++ который совместим с C99), и должен быть определен в заголовочном файле climits как максимально большое число, помещающееся в объект типа long long int. В climits так же есть еще несколько макро-определений (для минимально возможного числа и беззнаковые эквиваленты), но я решил использовать этот макрос, что не принципиально. Важно то, что, в отличие от boost, в данной реализации «большие» типы из Си не будут определяться как целочисленные константы, хоть они и (возможно) присутствуют в компиляторе. Что еще важно, так это типы char16_t и char32_t, которые тоже были введены в C++11, но вот их уже в С99 не завезли (они появились уже одновременно с C++ в стандарте Си C11), а потому в старых стандартах их определение может быть только через псевдоним типа (к примеру typedef short char16_t, но об этом позже). Раз так, то для того чтобы специализация шаблона корректно обрабатывала ситуации и когда эти типы отдельные (встроенные), и когда они определены через typedef, необходим еще один слой специализации шаблона detail::_is_integral.

Занимательный факт же состоит в том что в некоторых старых компиляторах эти Си-шные «большие» типы не являются integral constant. Что можно понять и даже простить, так как типы эти нестандартные для C++ до 11 стандарта, и вообще их там не должно быть. А вот что понять сложно, так это то, что данные типы в новейшем C++ компиляторе творчества Embarcadero (Embarcadero C++ Builder), который C++ 11 якобы поддерживает, все так же не являются integral constant в их 32-битных сборках (как и 20 лет назад, тогда это был Borland еще правда). Видимо из-за этого, в том числе, большая часть стандартной библиотеки C++ 11 отсутствует в этих самых 32-битных сборках (#include ratio? chrono? обойдетесь). Embarcadero похоже решило форсировать наступление 64-битной эры с девизом: «Хотите C++ 11 или новее стандарт? Собирайте 64-разрядную программу (и только clang, наш компилятор не может)!».

Закончив разбирательства с фундаментальными типами языка введем еще несколько простых шаблонов:

Простые шаблоны
template <bool, class _Tp = detail::void_type>
struct enable_if
{ };

template <class _Tp>
struct enable_if<true, _Tp>
{
    typedef _Tp type;
};

template<class, class>
struct is_same :
    public false_type
{ };

template<class _Tp>
struct is_same<_Tp, _Tp> :
    public true_type//specialization
{ };

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

template <class _Tp>
struct is_const<const _Tp> :
    public true_type
{ };

template <class _Tp>
struct is_const<const volatile _Tp> :
    public true_type
{ };

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

template<class _Tp>
struct is_volatile<volatile _Tp>
    : public true_type
{ };

template<class _Tp>
struct is_volatile<const volatile _Tp>
    : public true_type
{ };


Здесь заслуживает внимания разве только тот факт, что шаблоны специализируются для всех модификаторов типа (volatile и const volatile к примеру), т.к. некоторые компиляторы имеют тенденцию «терять» один из модификаторов при раскрытии шаблона.

Отдельно выделю реализацию is_signed и is_unsigned:

namespace detail
{
    template<bool>
    struct _sign_unsign_chooser;

    template<class _Tp>
    struct _signed_comparer
    {
        static const bool value = _Tp(-1) < _Tp(0);
    };

    template<class _Tp>
    struct _unsigned_comparer
    {
        static const bool value = _Tp(0) < _Tp(-1);
    };

    template<bool Val>
    struct _cat_base :
        integral_constant<bool, Val>
    {	// base class for type predicates
    };

    template<>
    struct _sign_unsign_chooser<true>//integral
    {
        template<class _Tp>
        struct _signed :
            public _cat_base<_signed_comparer<typename remove_cv<_Tp>::type>::value>
        {
        };

        template<class _Tp>
        struct _unsigned :
            public _cat_base<_unsigned_comparer<typename remove_cv<_Tp>::type>::value>
        {
        };
    };

    template<>
    struct _sign_unsign_chooser<false>//floating point
    {
        template<class _Tp>
        struct _signed :
            public is_floating_point<_Tp>
        {
        };

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

template<class T>
struct is_signed
{	// determine whether T is a signed type

    static const bool value = detail::_sign_unsign_chooser<is_integral<T>::value>::template _signed<T>::value;

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

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

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

template<class T>
struct is_unsigned
{	// determine whether T is an unsigned type

    static const bool value = detail::_sign_unsign_chooser<is_integral<T>::value>::template _unsigned<T>::value;

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

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

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

При реализации этой части я вступил в неравную схватку с Borland C++ Builder 6.0, который никак не хотел сделать эти два шаблона наследниками от integral_constant, что в итоге после десятков internal compiler error вылилось в «имитирование» поведения integral_constant для этих шаблонов. Здесь, возможно, стоит еще побороться и придумать какое то хитрое выведение типа is_*un*signed:integral_constant через шаблоны, но я отложил пока что эту задачу как не приоритетную. Интересно же в вышеприведенном участке кода то, как во время компиляции определяется что тип беззнаковый/знаковый. Для начала отметаются все не целочисленные типы и для них шаблон уходит в отдельную специализированную ветку _sign_unsign_chooser с шаблонным аргументом false, которая в свою очередь всегда возвращает value == false для любых типов, кроме стандартных типов с плавающей точкой (они всегда знаковые по очевидным причинам, так что _signed::value будет true). Для целочисленных же типов выполняются простые, но от того довольно занимательные проверки. Здесь используется тот факт, что для беззнаковых целочисленных типов при уменьшении и последующем «переходе» через минимум (0 очевидно) происходит переполнение и число приобретает свое максимально возможное значение.

Этот факт общеизвестен, так же как и тот что для знаковых типов переполнение является undefined behaviour и за этим нужно следить (согласно стандарту вы не можете уменьшить int переменную меньше чем INT_MIN и надеяться что в результате переполнения получите INT_MAX, а не 42 или отформатированный жесткий диск).

Запишем _Tp(-1) < _Tp(0) для проверки на «знаковость» типа используя данный факт, тогда для беззнаковых типов -1 «трансформируется» через переполнение в максимальное число данного типа, тогда как для знаковых такое сравнение будет проведено без переполнения, и сравниваться будет -1 с 0.

И последний на сегодня, но далеко не последний «трюк» в моей библиотеке это реализация alignment_of:

namespace detail
{
    template <class _Tp>
    struct _alignment_of_trick
    {
        char c;
        _Tp t;
        _alignment_of_trick();
    };

    template <unsigned A, unsigned S>
    struct _alignment_logic_helper
    {
        static const std::size_t value = A < S ? A : S;
    };

    template <unsigned A>
    struct _alignment_logic_helper<A, 0>
    {
        static const std::size_t value = A;
    };

    template <unsigned S>
    struct _alignment_logic_helper<0, S>
    {
        static const std::size_t value = S;
    };

    template< class _Tp >
    struct _alignment_of_impl
    {
    #if _MSC_VER > 1400
        //
        // With MSVC both the build in __alignof operator
        // and following logic gets things wrong from time to time
        // Using a combination of the two seems to make the most of a bad job:
        //
        static const std::size_t value =
            (_alignment_logic_helper<
                sizeof(_alignment_of_trick<_Tp>) - sizeof(_Tp),
                __alignof(_Tp)
            >::value);
    #else
        static const std::size_t value =
            (_alignment_logic_helper<
                sizeof(_alignment_of_trick<_Tp>) - sizeof(_Tp),
                sizeof(_Tp)
            >::value);
    #endif
        typedef integral_constant<std::size_t, std::size_t(_alignment_of_impl::value)> type;

    private:
        typedef intern::type_traits_asserts check;
        typedef typename check::alignment_of_type_can_not_be_zero_assert< _alignment_of_impl::value != 0 >::
            alignment_of_type_can_not_be_zero_assert_failed
        check1; // if you are there means aligment of type passed can not be calculated or compiler can not handle this situation (sorry, nothing can be done there)
    };

    // borland compilers seem to be unable to handle long double correctly, so this will do the trick:
    struct _long_double_wrapper{ long double value; };
}

template <class _Tp> 
struct alignment_of: 
    public detail::_alignment_of_impl<_Tp>::type
{};

template <class _Tp> 
struct alignment_of<_Tp&>: 
    public alignment_of<_Tp*>
{};

template<> 
struct alignment_of<long double>: 
    public alignment_of<detail::_long_double_wrapper>
{};

Здесь снова отличился Microsoft с их Visual Studio, которая даже обладая встроенным нестандартным __alignof build-in макросом все равно выдает некорректные результаты при его использовании.

Объяснение от boost
Visual C++ users should note that MSVC has varying definitions of «alignment». For example consider the following code:

typedef long long align_t;
assert(boost::alignment_of<align_t>::value % 8 == 0);
align_t a;
assert(((std::uintptr_t)&a % 8) == 0);
char c = 0;
align_t a1;
assert(((std::uintptr_t)&a1 % 8) == 0);

In this code, even though boost::alignment_of<align_t> reports that align_t has 8-byte alignment, the final assert will fail for a 32-bit build because a1 is not aligned on an 8 byte boundary. Note that had we used the MSVC intrinsic __alignof in place of boost::alignment_of we would still get the same result. In fact for MSVC alignment requirements (and promises) only really apply to dynamic storage, and not the stack.


Напомню что же должен делать шаблон std::alignment_of — возвращать значение, которое представляет собой требования к размещению элемента данного типа в памяти. Если немного отвлечься, то элемент каждого типа имеет какое то размещение в памяти, и если для массива элементов оно (размещение) непрерывно, то для, к примеру, классов вполне могут существовать «дырки» между элементами-членами класса (sizeof класса struct {char a;} будет скорее всего не равен 1, хотя там 1 байт всего внутри, т.к. компилятор выровняет его до 1 + 3 байт в процессе оптимизации).

А теперь посмотрим на код еще раз. Объявим структуру _alignment_of_trick, в которой разместим с «отступом» по памяти в 1 байт элемент проверяемого типа. И проверим выравнивание просто вычтя из размера получившейся структуры размер проверяемого типа. Тоесть в случае если компилятор решит «воткнуть» пустое место между элементом проверяемого типа и предыдущим char, то мы получим значение выравнивания типа в структуре.

Так же здесь впервые встречается static assert как тип. Они объявлены как:

namespace intern
{
    // since we have no static_assert in pre-C++11 we just compile-time assert this way:
    struct type_traits_asserts
    {
        template<bool>
        struct make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert;

        template<bool>
        struct make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert;

        template<bool>
        struct not_allowed_arithmetic_type_assert;

        template<bool>
        struct alignment_of_type_can_not_be_zero_assert;
    };

    template<>
    struct type_traits_asserts::make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert<true>
    {
        typedef bool make_signed_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert_failed;
    };

    template<>
    struct type_traits_asserts::make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert<true>
    {
        typedef bool make_unsigned_template_require_that_type_shall_be_a_possibly_cv_qualified_but_integral_type_assert_failed;
    };

    template<>
    struct type_traits_asserts::not_allowed_arithmetic_type_assert<true>
    {
        typedef bool not_allowed_arithmetic_type_assert_failed;
    };

    template<>
    struct type_traits_asserts::alignment_of_type_can_not_be_zero_assert<true>
    {
        typedef bool alignment_of_type_can_not_be_zero_assert_failed;
    };
}

По сути данные специализированные шаблоны нужны для замены static_assert из C++ 11, который располагается внутри определения класса. Такие assert более легковесны и узкоспециализированны чем общая реализация STATIC_ASSERT из главы 2, и позволяют не тянуть за собой заголовочный файл core.h в type_traits.

image Много шаблонов? Будет еще больше! На этом пока что остановимся, так как дальше пойдет увлекательный рассказ о совмещении шаблонного программирования с техникой SFINAE, а так же о том зачем мне пришлось написать небольшой кодогенератор.

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

Комментарии (8)


  1. FlexFerrum
    23.07.2018 21:05

    Посмотрел на код. На объяснения. Понял, что, похоже, было проще напедалить шаблон:
    template<typename T, int typeKind, bool isSigned, int bits, //>
    struct BuiltinTypeTraits {};


    И тупо специализировать этот шаблон для всех встроенных типов, чем городить огород из детекторов. Кода бы получилось меньше.


    И с long (как short, signed и unsigned) есть одна малоизвестная особенность: всё это модификаторы интегрального типа, а не часть его имени. :) То есть записи long signed int/long signed/long/signed long/signed long int значат одно и то же. :D


    1. oktonion Автор
      23.07.2018 21:21

      По поводу первого комментария — в смысле просто сделать шаблон где сразу указывается жестко что «вот этот тип беззнаковый, столько то бит», а «этот тип знаковый, столько то бит»?

      По поводу второй части с long. Так это по-моему широко известная особенность. Или это указание на мой косяк где-то? Я не очень понимаю какое место тогда.

      Малоизвестной особенностью здесь было бы указать что auto в C уже давно специальное слово, и раньше оно означало совсем не то что сейчас.)


      1. FlexFerrum
        23.07.2018 21:26
        +1

        По поводу первого комментария — в смысле просто сделать шаблон где сразу указывается жестко что «вот этот тип беззнаковый, столько то бит», а «этот тип знаковый, столько то бит»?

        Ага.
        По поводу второй части с long. Так это по-моему широко известная особенность.

        Ну мало ли, вдруг кто не знает? :)


        1. oktonion Автор
          23.07.2018 21:33

          Возможно, но хочу чтобы компилятор за меня думал кто там какой. А то больно много чести все ему руками объяснять. Вообще хочу чтобы компилятор все считал во время компиляции, а результатом был бы исполняемый файл где сразу выводился ответ. =)


          1. FlexFerrum
            24.07.2018 14:10

            Позволять думать компилятору можно тогда, когда компилятор адекватный. А когда не очень… Ну… :) Впрочем, каждый развлекается как хочет. :)


  1. oktonion Автор
    23.07.2018 21:32

    *промахнулся*


  1. alexeiz
    23.07.2018 23:43

    А может нужно было просто выбросить Borland C++ 6.0, из-за которого вся эта канитель, на помойку?


    1. oktonion Автор
      24.07.2018 08:29

      Во-первых не из-за него одного. Студия от Microsoft тоже приподносит много сюрпризов.
      Во-вторых выкинуть и заменить, так? Чем? "Продолжателем" от Embarcadero, где баги все те же, а еще десяток новых.
      В-третьих вы статью с начала читали? Я лично разрабатывал бы вообще на Visual Studio последней или на Qt с современным компилятором.


      Комментарий ваш конечно очевиден, но довольно бессодержателен. На этом давайте воздержимся от этой дискуссии и от подобных комментариев впредь.