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

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


Из-за ограничений на возможность использовать компиляторы 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
    16.07.2018 13:52
    +1

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


    1. oktonion Автор
      16.07.2018 13:57

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


  1. Sazonov
    16.07.2018 14:17

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


    1. oktonion Автор
      16.07.2018 14:52

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

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

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


      1. Sazonov
        16.07.2018 18:40

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


        1. oktonion Автор
          16.07.2018 18:51
          +1

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

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


          1. Sazonov
            17.07.2018 16:43

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

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


            1. oktonion Автор
              17.07.2018 17:01

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

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

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


        1. mayorovp
          16.07.2018 19:01
          -1

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


          1. tzlom
            18.07.2018 12:44

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


            1. mayorovp
              18.07.2018 12:48

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


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


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


    1. oktonion Автор
      16.07.2018 15:02

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


  1. palexab
    17.07.2018 11:45

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


    1. oktonion Автор
      17.07.2018 11:46

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


  1. host13
    17.07.2018 11:46

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


    1. mayorovp
      17.07.2018 12:01

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


    1. oktonion Автор
      17.07.2018 12:02

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


  1. orcy
    17.07.2018 21:08

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


    1. oktonion Автор
      17.07.2018 23:07

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


  1. orcy
    17.07.2018 21:08

    (deleted)