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

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

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


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

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

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

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

    Итак, продолжим.

    Оглавление


    Введение
    Глава 1. Viam supervadet vadens
    Глава 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
    Глава 3. Поиск идеальной реализации nullptr
    Глава 4. Шаблонная «магия» C++
    ....4.1 Начинаем с малого
    ....4.2 О сколько нам ошибок чудных готовит компиляций лог
    ....4.3 Указатели и все-все-все
    ....4.4 Что же еще нужно для шаблонной библиотеки
    Глава 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 и на какие еще баги в компиляторах я наткнулся при разработке.

    Благодарю за внимание.
    Поделиться публикацией
    Ой, у вас баннер убежал!

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

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

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

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

          Читать — это вам в текст стандарта разве что или доверять выдержкам из него на всяких stack overflow.
            0
            Благодарю за ответ. Это может понадобиться, если придётся велосипедить рефлексию/интроспекцию в рамках старых стандартов. Но, как я понимаю, всё равно любой метод (виртуальный или нет) будет представлять из себя обычную «thiscall» функцию. Т.е. имея правильный указатель на класс можно всё равно запихнуть указатель на метод в void*.
              +1
              На тему того как компактно хранить указатели на свободные функции и функции-члены класса можете взглянуть на fast delegates и понять какой это цирк, а так же на мое расширение всего этого безобразия .

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

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

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

                  А как покопаете информацию так напишите статью и поделитесь раскопками. Мне будет интересно, уверен сообществу тоже.
                    0
                    Благодарю (голосовать пока не могу). Если будет время — займусь раскопками и статьёй.
                –1
                Нет. Одна из возможных форм указателя на виртуальный метод — это смещение относительно начала vtable. Плюс нужно где-то хранить тип указателя, чтобы понять на обычный или на виртуальный метод он указывает…
                  0
                  Виртуальный он или нет — не важно, указатель на метод состоит из this и указателя на функцию-имплементацию метода. Если это обычный метод, то указатель на метод будет вычислен во время компиляции, если виртуальный — скопирован из vtable во время исполнения взятия указателя.
                    0

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


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


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

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

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

                0
                Нет, надеюсь не придется. Хотя может тут найдутся специалисты и из этой области.
                0
                Прошу прощения, не силен в C++, но вот это все нужно чтобы просто реализовать null?
                  0
                  Это все нужно чтобы реализовать nullptr на старой версии языка, где его нету.
                    0
                    Если вы про null из C# то да, он является аналогом nullptr из C++ (разве что к bool преобразовываться не умеет).
                    А на счет «просто реализовать» могу сказать что все это нужно чтобы сложно реализовать своими силами то, что должно быть просто реализовано по более новой редакции стандарта языка силами компилятора.
                    0
                    Я думал что SFINAE это какая-то продвинутая фича, если есть она то есть и nullptr. Но оказывается я ошибался. Довольно хардкорно выглядит
                      0
                      (deleted)
                        0
                        Принцип SFINAE работает не только для перегрузок функций, но и для шаблонов. Но здесь именно перегрузки функций работая как маркеры «да\нет» делают всю работу, потому и описал на этом примере.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое