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



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

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


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

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

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (20):

  1. sinc
    /#18886803 / +1

    универсальность, безусловно, дело хорошее, но столько текста ради константы…
    чем дальше, тем с++ все больше становится языком для компиляторов, а не для людей.

    • oktonion
      /#18886825

      Это должно быть скрыто в недрах кода компилятора, а наружу торчать ключевое слово nullptr и тип его. Не берусь судить о «человечности» C++, но столько текста у меня это именно результат:
      а) Отсутствия C++ 11 и велосипедостроения.
      б) Моей дотошности до соответствия nullptr стандарту.
      в) Багов старых компиляторов.

  2. Sazonov
    /#18886899

    Возможно я немного не в тему, но уже долгое время мучает вопрос. Вы написали, что void * по стандарту должен вмещать в себя любой указатель. Касается ли это указателей на виртуальные методы? И где вообще можно почитать про sizeof указателя на виртуальный метод?

    • oktonion
      /#18887023

      Если кратко то ответ «нет». Указатели на члены класса это отдельные указатели и их нельзя держать в void*. Для этого есть тип указатель-на-член-класса со своим объявлением. У меня речь идет о том что nullptr должен уметь преобразовываться в любой указатель, это обратная операция. По стандарту sizeof(nullptr) == sizeof(void*), потому речь идет именно о указателе void.

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

      Читать — это вам в текст стандарта разве что или доверять выдержкам из него на всяких stack overflow.

      • Sazonov
        /#18887621

        Благодарю за ответ. Это может понадобиться, если придётся велосипедить рефлексию/интроспекцию в рамках старых стандартов. Но, как я понимаю, всё равно любой метод (виртуальный или нет) будет представлять из себя обычную «thiscall» функцию. Т.е. имея правильный указатель на класс можно всё равно запихнуть указатель на метод в void*.

        • oktonion
          /#18887641 / +1

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

          Не понимаю в чем «правильность» может быть у указателя на класс. Указатель на функцию-член класса в void* запихнуть не получится, т.к. у него размер не тот, вы информацию потеряете. А вот хранить связку this_pointer + указатель-на-функцию-член это пожалуйста.

          • Sazonov
            /#18890689

            Под правильностью я понимал случаи с полиморфными классами со сложной иерархией наследования. Насколько я понимаю, нельзя «в лоб» получить нужный тип из void * при помощи static_cast при ромбовидном наследовании. Но я не про это.

            Допустим, у нас есть виртуальный метод MyClass::Foo( args... );. Соответственно при его вызове неявно первым аргументом передаётся this. Т.е. зная адрес метода в памяти и зная this этот метод можно вызвать как обычную функцию. При этом адрес метода всё равно будет равен разрядности платформы, т.е. sizeof(void*). Это верно?
            Но я думаю, что мне лучше не вас в комментариях грузить, а пойти покопать информацию про способы вызова функций. Заранее спасибо.

            • oktonion
              /#18890803

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

              Это верно, естественно адреса будут в итоге по разрядности платформы. На этот счет коллеги лучше знакомые с ассемблером вам наверняка много чего занимательного расскажут. Верно и то, что неявно передается this как аргумент вызываемой функции. Но только это к языку C++ и типу языка void* уже имеет слабое отношение. Этими вещами занимается компилятор с транслятором, и как они это делают зависит от того как разработчики компилятора это реализовали. Если вас интересует внутреннее представление виртуальных таблиц в разных компиляторах, то я вам ссылку привел на SO выше.

              А как покопаете информацию так напишите статью и поделитесь раскопками. Мне будет интересно, уверен сообществу тоже.

        • mayorovp
          /#18887653 / -1

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

          • tzlom
            /#18894191

            Виртуальный он или нет — не важно, указатель на метод состоит из this и указателя на функцию-имплементацию метода. Если это обычный метод, то указатель на метод будет вычислен во время компиляции, если виртуальный — скопирован из vtable во время исполнения взятия указателя.

            • mayorovp
              /#18894209

              Вообще-то нет. this передается уже при вызове и частью указателя не является.


              Напомню синтаксис: (foo -> *bar) (x, y) (здесь bar — указатель на метод)


              А вот где вы будете искать функцию-имплементацию метода когда она зависит от foo — интересный вопрос...

    • oktonion
      /#18887071

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

  3. palexab
    /#18889189

    С ноткой грусти А вы не пробовали OpenCV под Builder собрать? :)

    • oktonion
      /#18889193

      Нет, надеюсь не придется. Хотя может тут найдутся специалисты и из этой области.

  4. host13
    /#18889197

    Прошу прощения, не силен в C++, но вот это все нужно чтобы просто реализовать null?

    • mayorovp
      /#18889267

      Это все нужно чтобы реализовать nullptr на старой версии языка, где его нету.

    • oktonion
      /#18889273

      Если вы про null из C# то да, он является аналогом nullptr из C++ (разве что к bool преобразовываться не умеет).
      А на счет «просто реализовать» могу сказать что все это нужно чтобы сложно реализовать своими силами то, что должно быть просто реализовано по более новой редакции стандарта языка силами компилятора.

  5. orcy
    /#18891831

    Я думал что SFINAE это какая-то продвинутая фича, если есть она то есть и nullptr. Но оказывается я ошибался. Довольно хардкорно выглядит

    • oktonion
      /#18892291

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

  6. orcy
    /#18891833

    (deleted)