С сожалением об отсутствии в C++ полноценного static if или…

    … как наполнить шаблонный класс разным содержимым в зависимости от значений параметров шаблона?


    Когда-то, уже довольно давно, язык D начали делать как "правильный C++" с учетом накопившегося в C++ опыта. Со временем D стал не менее сложным и более выразительным языком, чем C++. И уже C++ стал подсматривать за D. Например, появившийся в C++17 if constexpr, на мой взгляд, — это прямое заимствование из D, прототипом которому послужил D-шный static if.


    К моему сожалению, if constexpr в С++ не обладает такой же мощью, как static if в D. Тому есть свои причины, но все-таки бывают случаи, когда остается только пожалеть, что if constexpr в C++ не позволяет управлять наполнением C++ного класса. Об одном из таких случаев и хочется поговорить.


    Речь пойдет о том, как сделать шаблонный класс, содержимое которого (т.е. состав методов и логика работы некоторых из методов) менялось бы в зависимости от того, какие параметры были переданы этому шаблонному классу. Пример взят из реальной жизни, из опыта разработки новой версии SObjectizer-а.


    Задача, которую требуется решить


    Требуется создать хитрый вариант "умного указателя" для хранения объектов-сообщений. Чтобы можно было написать что-то вроде:


    message_holder_t<my_message> msg{ new my_message{...} };
    send(target, msg);
    send(another_target, msg);

    Хитрость этого класса message_holder_t в том, что нужно учесть три важных фактора.


    От чего отнаследован тип сообщения?


    Типы сообщений, которыми параметризуется message_holder_t, делятся на две группы. Первая группа — это сообщения, которые наследуются от специального базового типа message_t. Например:


    struct so5_message final : public so_5::message_t {
       int a_;
       std::string b_;
       std::chrono::milliseconds c_;
    
       so5_message(int a, std::string b, std::chrono::milliseconds c)
          : a_{a}, b_{std::move(b)}, c_{c}
       {}
    };

    В этом случае message_holder_t внутри себя должен содержать только указатель на объект этого типа. Этот же указатель должен возвращаться в методах-getter-ах. Т.е., для случая наследника от message_t должно быть что-то вроде:


    template<typename M>
    class message_holder_t {
       intrusive_ptr_t<M> m_msg;
    public:
       ...
       const M * get() const noexcept { return m_msg.get(); }
    };

    Вторая группа — это сообщения произвольных пользовательских типов, которые не наследуются от message_t. Например:


    struct user_message final {
       int a_;
       std::string b_;
       std::chrono::milliseconds c_;
    
       user_message(int a, std::string b, std::chrono::milliseconds c)
          : a_{a}, b_{std::move(b)}, c_{c}
       {}
    };

    Экземпляры таких типов в SObjectizer-е отсылаются не сами по себе, а заключенными в специальную обертку user_type_message_t<M>, которая уже наследуется от message_t. Поэтому для таких типов message_holder_t должен содержать внутри себя указатель на user_type_message_t<M>, а методы-getter-ы должны возвращать указатель на M:


    template<typename M>
    class message_holder_t {
       intrusive_ptr_t<user_type_message_t<M>> m_msg;
    public:
       ...
       const M * get() const noexcept { return std::addressof(m_msg->m_payload); }
    };

    Иммутабельность или мутабельность сообщений


    Второй фактор — это деление сообщений на неизменяемые (immutable) и изменяемые (mutable). Если сообщение неизменяемое (а по умолчанию оно неизменяемое), то методы-getter-ы должны возвращать константный указатель на сообщение. А если изменяемое, то getter-ы должны возвращать не константный указатель. Т.е. должно быть что-то вроде:


    message_holder_t<so5_message> msg1{...}; // Неизменяемое сообщение.
    const int a = msg1->a_; // OK.
    msg1->a_ = 0; // ТУТ ДОЛЖНА БЫТЬ ОШИБКА КОМПИЛЯЦИИ!
    
    message_holder_t<mutable_msg<user_message>> msg2{...}; // Изменяемое сообщение.
    const int a = msg2->a_; // OK.
    msg2->a_ = 0; // OK.

    shared_ptr vs unique_ptr


    Третий фактор — это логика поведения message_holder_t как умного указателя. Когда-то он должен вести себя как std::shared_ptr, т.е. можно иметь несколько message_holder-ов, ссылающихся на один и тот же экземпляр сообщения. А когда-то он должен вести себя как std::unique_ptr, т.е. только один экземпляр message_holder-а может ссылаться на экземпляр сообщения.


    По умолчанию, поведение message_holder_t должно зависеть от изменяемости/неизменяемости сообщения. Т.е. с неизменяемыми сообщениями message_holder_t должен вести себя как std::shared_ptr, а с изменяемыми, как std::unique_ptr:


    message_holder_t<so5_message> msg1{...};
    message_holder_t<so5_message> msg2 = msg; // OK.
    
    message_holder_t<mutable_msg<user_message>> msg3{...};
    message_holder_t<mutable_msg<user_message>> msg4 = msg3; // БУМС! Так нельзя!
    message_holder_t<mutable_msg<user_message>> msg5 = std::move(msg3); // OK.

    Но жизнь штука сложная, поэтому нужно иметь еще и возможность вручную задать поведение message_holder_t. Чтобы можно было сделать message_holder-а для иммутабельного сообщения, который ведет себя как unique_ptr. И чтобы можно было сделать message_holder-а для изменяемого сообщения, который ведет себя как shared_ptr:


    using unique_so5_message = so_5::message_holder_t<
       so5_message,
       so_5::message_ownership_t::unique>;
    
    unique_so5_message msg1{...};
    unique_so5_message msg2 = msg1; // БУМС! Так нельзя!
    unique_so5_message msg3 = std::move(msg); // OK, сообщение в msg3.
    
    using shared_user_messsage = so_5::message_holder_t<
       so_5::mutable_msg<user_message>,
       so_5::message_ownership_t::shared>;
    
    shared_user_message msg4{...};
    shared_user_message msg5 = msg4; // OK.

    Соответственно, когда message_holder_t работает как shared_ptr, у него должен быть обычный набор конструкторов и операторов присваивания: и копирования, и перемещения. Кроме того, должен быть константный метод make_reference, который возвращает копию хранящегося внутри message_holder_t указателя.


    А вот когда message_holder_t работает как unique_ptr, то конструктор и оператор копирования у него должны быть запрещены. А метод make_reference должен изымать указатель у объекта message_holder_t: после вызова make_reference исходный message_holder_t должен остаться пустым.


    Чуть более формально


    Итак, нужно создать шаблонный класс:


    template<
       typename M,
       message_ownership_t Ownership = message_ownership_t::autodetected>
    class message_holder_t {...};

    у которого:


    • внутри должен храниться intrusive_ptr_t<M> или intrusive_ptr<user_type_message_t<M>> в зависимости от того, наследуется ли M от message_t;
    • методы-getter-ы должны возвращать либо const M*, либо M* в зависимости от изменяемости/неизменяемости сообщения;
    • должен быть либо полный набор конструкторов и операторов копирования/перемещения, либо только конструктор и оператор перемещения;
    • метод make_reference() должен либо возвращать копию хранимого intrusive_ptr, либо должен изымать значение intrusive_ptr и оставлять исходный message_holder_t в пустом состоянии. В первом случае make_reference() должен быть константным, во втором — неконстантным методом.

    Последние два пункта из перечня определяются параметром Ownership (а также мутабельностью сообщения, если для Ownership используется значение autodetected).


    Как это было решено


    В данном разделе мы рассмотрим все составляющие, из которых получилось итоговое решение. Ну и само результирующее решение. Будут показаны максимально очищенные от всех отвлекающих внимание деталей фрагменты кода. Если кого-то интересует реальный код, то увидеть его можно здесь.


    Disclaimer


    Показанное ниже решение не претендует на красоту, идеальность или образец для подражания. Оно было найдено, реализовано, протестировано и задокументированно за небольшое время, под давлением сроков. Возможно, если бы времени было больше, и поиском решения занимался более молодой, толковый и сведующий в современном C++ разработчик, то оно получилось бы компактнее, проще и понятнее. Но, как получилось, так и получилось… "Don't shoot the pianist", в общем.


    Последовательность шагов и уже готовая шаблонная магия


    Итак, нам нужно иметь класс с несколькими наборами методов. Содержимое этих наборов должно откуда-то взяться. Откуда?


    В языке D мы могли бы воспользоваться static if и определить разные части класса в зависимости от разных условий. В каком-нибудь Ruby мы могли бы подмешать методы в свой класс посредством метода include. Но мы в C++, в котором пока наши возможности сильно ограничены: мы можем либо определить метод/атрибут прямо внутри класса, либо можем унаследовать метод/атрибут из какого-то базового класса.


    Определить разные методы/атрибуты внутри класса в зависимости от какого-то условия мы не можем, т.к. C++ный if constexpr — это не D-шный static if. Следовательно, остается только наследование.


    Upd. Как мне подсказали в комментариях, тут следует высказаться более осторожно. Поскольку в C++ есть SFINAE, то мы посредством SFINAE можем включать/выключать видимость отдельных методов в классе (т.е. достигать эффекта, аналогичного static if-у). Но у такого подхода есть два серьезных, на мой взгляд, недостатка. Во-первых, если таких методов не 1-2-3, а 4-5 или больше, то оформлять каждый из них посредством SFINAE утомительно, да и на читабельности кода это сказывается. Во-вторых, SFINAE не помогает нам добавлять/изымать атрибуты (поля) класса.

    В C++ мы можем определить несколько базовых классов, от которых мы затем отнаследуем message_holder_t. А выбор того или иного базового класса уже будем делать в зависимости от значений параметров шаблона, посредством std::conditional.


    Но фокус в том, что нам потребуется не просто набор базовых классов, а небольшая цепочка наследования. В ее начале будет класс, который будет определять общую функциональность, которая потребуется в любом случае. Далее будут базовые классы, которые будут определять логику поведения "умного указателя". А уже затем будет класс, который определит нужные getter-ы. В таком порядке мы и рассмотрим реализованные классы.


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


    Общая база для хранения указателя


    Начнем с общего базового типа, который хранит соответствующий intrusive_ptr, а также предоставляет общий набор методов, которые нужны любой из реализаций message_holder_t:


    template< typename Payload, typename Envelope >
    class basic_message_holder_impl_t
       {
       protected :
          intrusive_ptr_t< Envelope > m_msg;
    
       public :
          using payload_type = Payload;
          using envelope_type = Envelope;
    
          basic_message_holder_impl_t() noexcept = default;
    
          basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept
             :  m_msg{ std::move(msg) }
             {}
    
          void reset() noexcept { m_msg.reset(); }
    
          [[nodiscard]]
          bool empty() const noexcept { return static_cast<bool>( m_msg ); }
    
          [[nodiscard]]
          operator bool() const noexcept { return !this->empty(); }
    
          [[nodiscard]]
          bool operator!() const noexcept { return this->empty(); }
       };

    У этого шаблонного класса два параметра. Первый, Payload, задает тип, который должны использовать методы-getter-ы. Тогда как второй, Envelope, задает тип для intrusive_ptr. В случае, когда тип сообщения наследуется от message_t оба эти параметра будут иметь одинаковое значение. А вот если сообщение не наследуется от message_t, тогда в качестве Payload будет тип сообщения, а в качестве Envelope будет выступать user_type_message_t<Payload>.


    Думаю, что в основном содержимое этого класса не вызывает вопросов. Но отдельно следует обратить внимание на две вещи.


    Во-первых, сам указатель, т.е. атрибут m_msg, определен в protected секции для того, чтобы классы наследники имели к нему доступ.


    Во-вторых, для этого класса сам компилятор генерирует все необходимые конструкторы и операторы копирования/перемещения. И на уровне этого класса мы пока ничего не запрещаем.


    Отдельные базы для shared_ptr- и unique_ptr-поведения


    Итак, у нас есть класс, который хранит указатель на сообщение. Теперь мы можем определить его наследников, которые и будут вести себя либо как shared_ptr, либо как unique_ptr.


    Начнем со случая shared_ptr-поведения, т.к. здесь меньше всего кода:


    template< typename Payload, typename Envelope >
    class shared_message_holder_impl_t
       :  public basic_message_holder_impl_t<Payload, Envelope>
       {
          using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>;
    
       public :
          using direct_base_type::direct_base_type;
    
          [[nodiscard]] intrusive_ptr_t< Envelope >
          make_reference() const noexcept
             {
                return this->m_msg;
             }
       };

    Ничего сложного: наследуемся от basic_message_holder_impl_t, наследуем все его конструкторы и определяем простую, неразрушающую реализацию make_reference().


    Для случая unique_ptr-поведения кода побольше, хотя сложного в нем ничего нет:


    template< typename Payload, typename Envelope >
    class unique_message_holder_impl_t
       :  public basic_message_holder_impl_t<Payload, Envelope>
       {
          using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>;
    
       public :
          using direct_base_type::direct_base_type;
    
          unique_message_holder_impl_t(
             const unique_message_holder_impl_t & ) = delete;
    
          unique_message_holder_impl_t(
             unique_message_holder_impl_t && ) = default;
    
          unique_message_holder_impl_t &
          operator=( const unique_message_holder_impl_t & ) = delete;
    
          unique_message_holder_impl_t &
          operator=( unique_message_holder_impl_t && ) = default;
    
          [[nodiscard]] intrusive_ptr_t< Envelope >
          make_reference() noexcept
             {
                return { std::move(this->m_msg) }; 
             }
       };

    Опять же, наследуемся от basic_message_holder_impl_t и наследуем у него нужные нам конструкторы (это конструктор по-умолчанию и инициализирующий конструктор). Но при этом определяем конструкторы и операторы копирования/перемещения в соответствии с логикой unique_ptr: копирование запрещаем, перемещение реализуем.


    Также у нас здесь разрушающий метод make_reference().


    Вот, собственно, все. Осталось только реализовать выбор между двумя этими базовыми классами...


    Выбор между shared_ptr- и unique_ptr-поведением


    Для выбора между shared_ptr- и unique_ptr-поведением потребуется следующая метафункция (метафункция она потому, что "работает" с типами в компайл-тайм):


    template< typename Msg, message_ownership_t Ownership >
    struct impl_selector
       {
          static_assert( !is_signal<Msg>::value,
                "Signals can't be used with message_holder" );
    
          using P = typename message_payload_type< Msg >::payload_type;
          using E = typename message_payload_type< Msg >::envelope_type;
    
          using type = std::conditional_t<
                message_ownership_t::autodetected == Ownership,
                   std::conditional_t<
                         message_mutability_t::immutable_message ==
                               message_mutability_traits<Msg>::mutability,
                         shared_message_holder_impl_t<P, E>,
                         unique_message_holder_impl_t<P, E> >,
                   std::conditional_t<
                         message_ownership_t::shared == Ownership,
                         shared_message_holder_impl_t<P, E>,
                         unique_message_holder_impl_t<P, E> >
             >;
       };

    Эта метафункция принимает оба параметра из списка параметров message_holder_t и в качестве результата (т.е. определения вложенного типа type) "возвращает" тип, от которого следует отнаследоваться. Т.е. либо shared_message_holder_impl_t, либо unique_message_holder_impl_t.


    Внутри определения impl_selector можно увидеть следы той магии, о которой говорилось выше, и в которую мы не углублялись: message_payload_type<Msg>::payload_type, message_payload_type<Msg>::envelope_type и message_mutability_traits<Msg>::mutability.


    А для того, чтобы использовать метафункцию impl_selector было проще, следом определим более короткое имя для нее:


    template< typename Msg, message_ownership_t Ownership >
    using impl_selector_t = typename impl_selector<Msg, Ownership>::type;

    База для getter-ов


    Итак, у нас уже есть возможность выбрать базу, которая содержит указатель и определяет поведение "умного указателя". Теперь нужно снабдить эту базу методами-getter-ами. Для чего нам потребуется один простой класс:


    template< typename Base, typename Return_Type >
    class msg_accessors_t : public Base
       {
       public :
          using Base::Base;
    
          [[nodiscard]] Return_Type *
          get() const noexcept
             {
                return get_ptr( this->m_msg );
             }
    
          [[nodiscard]] Return_Type &
          operator * () const noexcept { return *get(); }
    
          [[nodiscard]] Return_Type *
          operator->() const noexcept { return get(); }
       };

    Это шаблонный класс, который зависит от двух параметров, но их смысл уже совсем другой. В качестве параметра Base будет выступать результат показанной выше метафункции impl_selector. Т.е. в качестве параметра Base задается базовый класс, от которого нужно отнаследоваться.


    Важно отметить, что если наследование происходит от unique_message_holder_impl_t, у которого конструктор и оператор копирования запрещены, то компилятор не сможет сгенерировать конструктор и оператор копирования для msg_accessors_t. Что нам и требуется.


    В качестве параметра Return_Type будет выступать тип сообщения, указатель/ссылку на который будет возвращаться getter-ами. Фокус в том, что для иммутабельного сообщения типа Msg параметр Return_Type будет иметь значение const Msg. Тогда как для мутабельного сообщения типа Msg параметр Return_Type будет иметь значение Msg. Таким образом метод get() для иммутабельных сообщений будет возвращать const Msg*, а для мутабельных — просто Msg*.


    Посредством свободной функции get_ptr() решается проблема работы с сообщениями, которые не отнаследованны от message_t:


    template< typename M >
    M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept
       {
          return msg.get();
       }
    
    template< typename M >
    M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept
       {
          return std::addressof(msg->m_payload);
       }

    Т.е. если сообщение не наследуется от message_t и хранится как user_type_message_t<Msg>, то вызывается вторая перегрузка. А если наследуется, то первая перегрузка.


    Выбор конкретной базы для getter-ов


    Итак, шаблон msg_accessors_t требует два параметра. Первый вычисляется метафункцией impl_selector. Но для того, чтобы сформировать конкретный базовый тип из msg_accessors_t, нам нужно определиться со значением второго параметра. Для этого предназначена еще одна метафункция:


    template< message_mutability_t Mutability, typename Base >
    struct accessor_selector
       {
          using type = std::conditional_t<
                message_mutability_t::immutable_message == Mutability,
                msg_accessors_t<Base, typename Base::payload_type const>,
                msg_accessors_t<Base, typename Base::payload_type> >;
       };

    Обратить внимание можно разве что на вычисление параметра Return_Type. Один из тех немногих случаев, когда east const оказывается полезен ;)


    Ну и, для повышения читабельности последующего кода, более компактный вариант для работы с ней:


    template< message_mutability_t Mutability, typename Base >
    using accessor_selector_t = typename accessor_selector<Mutability, Base>::type;

    Итоговый наследник message_holder_t


    Теперь можно посмотреть на то, что же из себя представляет message_holder_t, для реализации которого потребовались все эти базовые классы и метафункции (из реализации удалена часть методов для конструирования экземпляра хранящегося в message_holder-е сообщения):


    template<
       typename Msg,
       message_ownership_t Ownership = message_ownership_t::autodetected >
    class message_holder_t
       :  public details::message_holder_details::accessor_selector_t<
                details::message_mutability_traits<Msg>::mutability,
                details::message_holder_details::impl_selector_t<Msg, Ownership> >
       {
          using base_type = details::message_holder_details::accessor_selector_t<
                details::message_mutability_traits<Msg>::mutability,
                details::message_holder_details::impl_selector_t<Msg, Ownership> >;
    
       public :
          using payload_type = typename base_type::payload_type;
          using envelope_type = typename base_type::envelope_type;
    
          using base_type::base_type;
    
          friend void
          swap( message_holder_t & a, message_holder_t & b ) noexcept
             {
                using std::swap;
                swap( a.message_reference(), b.message_reference() );
             }
       };

    По сути все то, что мы разбирали выше, потребовалось для того, чтобы записать вот этот "вызов" двух метафункций:


    details::message_holder_details::accessor_selector_t<
                details::message_mutability_traits<Msg>::mutability,
                details::message_holder_details::impl_selector_t<Msg, Ownership> >

    Т.к. это не первый вариант, а результат упрощения и сокращения кода, то могу сказать, что компактные формы метафункций ну очень сильно уменьшают объем кода и увеличивают его понятность (если о понятности здесь вообще уместно говорить).


    А что было бы, если бы...


    А вот если бы в C++ if constexpr был настолько же мощен, как static if в D, то можно было бы написать что-то вроде:


    Гипотетический вариант с более продвинутым if constexpr
    template<
       typename Msg,
       message_ownership_t Ownership = message_ownership_t::autodetected >
    class message_holder_t
       {
          static constexpr const message_mutability_t Mutability =
                details::message_mutability_traits<Msg>::mutability;
    
          static constexpr const message_ownership_t Actual_Ownership =
                (message_ownership_t::unique == Ownership ||
                   (message_mutability_t::mutable_msg == Mutability &&
                    message_ownership_t::autodetected == Ownership)) ?
                message_ownership_t::unique : message_ownership_t::shared;
    
       public :
          using payload_type = typename message_payload_type< Msg >::payload_type;
          using envelope_type = typename message_payload_type< Msg >::envelope_type;
    
       private :
          using getter_return_type = std::conditional_t<
                message_mutability_t::immutable_msg == Mutability,
                payload_type const,
                payload_type >;
    
       public :
          message_holder_t() noexcept = default;
    
          message_holder_t(
             intrusive_ptr_t< envelope_type > mf ) noexcept
             : m_msg{ std::move(mf) }
             {}
    
    if constexpr(message_ownership_t::unique == Actual_Ownership )
       {
          message_holder_t(
             const message_holder_t & ) = delete;
    
          message_holder_t(
             message_holder_t && ) noexcept = default;
    
          message_holder_t &
          operator=( const message_holder_t & ) = delete;
    
          message_holder_t &
          operator=( message_holder_t && ) noexcept = default;
       }
    
          friend void
          swap( message_holder_t & a, message_holder_t & b ) noexcept
             {
                using std::swap;
                swap( a.m_msg, b.m_msg );
             }
    
          [[nodiscard]] getter_return_type *
          get() const noexcept
             {
                return get_const_ptr( m_msg );
             }
    
          [[nodiscard]] getter_return_type &
          operator * () const noexcept { return *get(); }
    
          [[nodiscard]] getter_return_type *
          operator->() const noexcept { return get(); }
    
    if constexpr(message_ownership_t::shared == Actual_Ownership)
       {
          [[nodiscard]] intrusive_ptr_t< envelope_type >
          make_reference() const noexcept
             {
                return m_msg;
             }
       }
    else
       {
          [[nodiscard]] intrusive_ptr_t< envelope_type >
          make_reference() noexcept
             {
                return { std::move(m_msg) };
             }
       }
    
       private :
          intrusive_ptr_t< envelope_type > m_msg;
       };

    Как по мне, так отличия слишком уж разительны. И они не в пользу текущего C++ :(
    (разобранный выше C++ный код в виде одной сплошной "портянки" можно увидеть здесь).


    Кстати говоря, я не очень сильно слежу за тем, что происходит в области предложений по метапрограммированию и рефлексии для будущих версий С++. Но из того, что помню, складывается ощущение, что предлагавшиеся Саттером метаклассы не очень упростят вот эту конкретную задачу. Как я понимаю, посредством метаклассов можно будет написать генератор классов message_holder_t. Может быть такой генератор получится и несложным в написании, но вряд ли такой подход в данном конкретном случае окажется выразительнее и понятнее, чем в случае действительно продвинутого if constexpr.


    Заключение


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


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


    Тем не менее, сам факт того, что на С++ можно такое сотворить, меня лично радует. Огорчает количество труда и объем кода, который для этого потребуется. Но, надеюсь, что со временем объем этого кода и его сложность будет только сокращаться. В принципе, это видно уже сейчас. Ибо для C++98/03 я даже не взялся бы такой трюк проделывать, тогда как начиная с C++11 делать подобное становится все проще и проще.

    Поделиться публикацией

    Комментарии 90

      +1
      Ну, мне не кажется что вариант в Gist'е и гипотетический вариант равносильны. В Gist'е все по максимуму разложено на стратегии, а глядя на гипотетический, создается впечатление, что нужно просто реализовать одну функцию отдельно для shared и unique стратегии, а от полученного отнаследовать все остальное. В результате получится чуть больше кода, но читать и поддерживать это будет легче, чем простыню constexpr if'ов.
      Что до constexpr if'а как такового — тут две стороны медали. С одной стороны, для несложных случаев это экономит нажатия на клавиатуру, с другой — как я уже сказал, думаю, удобнее поддерживать грамотное разложение на стратегии, чем кашу из условий.
        0

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


        Тогда как при использовании static if вы просто линейно читаете код. Да, если там будут запутанные условия, то придется поразбираться с тем, что включается, а что исключается из рассмотрения. Но, при наличии запутанных условий, код и раздельными классами-стратегиями окажется так же сложнее (по крайней мере метафункции-селекторы усложнятся).


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

          +3
          Стратегии существуют не для того что бы заменить static if, а для того, что бы абстрагировать конкретные реализации. Например, можно завернуть в стратегию функционал создания/копирования/перемещения указателя, и реализовать стратегию unique и стратегию shared. Классов много, но не нужно держать в голове конкретную реализацию.
          А о наследовании, кстати, вопрос — а зачем вообще все от всего пронаследовано? Если Вам нужен разный интерфейс в зависимости от Ownership — это решается частичной специализацией.
            0
            Если Вам нужен разный интерфейс в зависимости от Ownership — это решается частичной специализацией.

            Частичная специализация хорошо работает, когда селекция между специализациями делается по одному признаку. Когда же она происходит по нескольким признакам, то специализации начинают тяготеть к копипасте. Вот как в данном случае.

              0
              Вот как раз в данном случае имеем два варианта класса — в зависимости от Actual_Ownership меняются copy конструктор/operator= и метод make_reference. Выносим вывод Actual_Ownership в отдельный класс, делаем две частичные специализации для shared и unique — все, holder готов, кода почти столько же, сколько в гипотетическом варианте, но зато более читабельно.
                0
                Читабельность понятие субъективное, поэтому позволю себе усомниться в том, что читабельность варианта с несколькими классами и частичной специализацией отдельных их частей будет выше, чем простой линейный код с парой static if-ов.

                Ну и, все так же остается вопрос о том, как все это документировать.
            +1
            А если 'static' на '#' заменить?))))
              0
              Т.е. обойтись только препроцессором?
                0
                Ну, только препроцессором может не получится, там же compile time integer expression требуется, если мне склероз не изменяет.
                А если по сути, то и if constexpr, и static if могут использоваться для условной компиляции. Вам нужно как раз такое применение. У if constexpr ограничения применимости строже, что, на мой взгляд, лучше, но Вам это бьет по рукам.

                Если по существу, то если с помощью enable_if::value и т.д. можно получить compile time integer expression в контексте препроцессора, то вот Вам и static if. Но брателлос сомневаются…
                  0
                  Если по существу, то если с помощью enable_if::value и т.д. можно получить compile time integer expression в контексте препроцессора

                  Это вряд ли. Насколько помню, сперва отрабатывает препроцессор и лишь затем, на препроцессированный, очищенный от макросов и пр. результат натравливается C++ный front-end. Так что на этапе препроцессинга никаких enable_if нет.

          +1
          я дико извиняюсь за вопрос не по теме, но вот тут
          : a_{a}, b_{std::move(b)}, c_{c}

          фигурные скобки вместо круглых зачем? это какая-то новая мода, пихать их везде, где компилятор не ругается?
            +2
            Мода. Началась с C++11.
              +1
              Это вызов абсолютно другой сущности (initializer_list), а круглые скобки это обычный конструктор.

              В банальных ситуациях когда хочется, например, добавить в коллекцию не один элемент, а, например, три то круглые скобки уже банально не работают и initializer_list решает проблему без необходимости руками писать цикл ;) Ну а если писать много «шаблонно-макросной магии» (много код-генерации) то такая запись ОЧЕНЬ сильно жизнь упрощает.

              Так что это не «мода», так лучше.

              P.S. поясню на счёт макросов: без макросов в C++ жить нельзя ибо банальное выведение имени переменной, даже в полностью шаблонном классе, не реализуемо вообще никак, а потому приходится «страдать». Ждём static reflection и жуём кактус :'(
              –3

              Ожидал прочитать про SFINAE и увидеть старый-добрый std::enable_if. Не угадал. Вопрос к автору, особенно после прочтения


              Определить разные методы внутри класса в зависимости от какого-то условия мы не можем

              Автор умеет в SFINAE или кто?

                +1
                Если нужно вводить методы пачками по 3-4-5 и более штук, то обкладывать каждый из них SFINAE… Ну то еще удовольствие.
                  0

                  Ну допустим соглашусь, что удовольствие может быть и сомнительное. Хотя в какой-то мере вопрос привычки.


                  Но сама процитированная выше фраза ставит в тупик — как так "не можем", это же неправда.

                    0
                    Так если есть серьезные препятствия (например, в виде резкого ухудшения читаемости кода), то это же невозможность и есть. Нет?

                    Кроме того, если методы требуют для себя еще какие-то специфические данные, то вот дополнительные данные в класс мы же через SFINAE не засунем. Тогда как через условное наследование — без проблем.

                    Т.е. получается, что условное наследование мощнее и читабельнее, чем SFINAE.
                      +1
                      серьезные препятствия [...] это же невозможность и есть

                      Ну извините, тут Вы хватили, это уже демагогия на пустом месте. Невозможность — это не когда "серьёзные препятствия", а когда возможности совсем нет, по крайней мере без серьёзного изменения исходных. Тем более что "читаемость кода" никак нельзя считать "серьёзным препятствием" — это, во-первых, категория чисто субъективная, а во-вторых, ей жертвуется постоянно по любому поводу.


                      условное наследование мощнее и читабельнее

                      Касательно "читабельнее" — тоже субъективно, об этом можно бесконечно и бестолково спорить. Мощнее да.


                      Но в процитированной выше фразе не шло речи об определениях новых полей класса. Только методов.


                      Вы как-то не к месту выкручиваетесь и демагогию разводите, мне кажется. Я может неудачно исходное сообщение сформулировал, но его надо в слегка шутливом тоне читать, а не в конфронтационном — так что смысла в глухую оборону уходить нет, задачи Вас как-то оскорбить или унизить у меня не было и нет.


                      Вопрос тем не менее вполне серьёзный — в статье, на мой взгляд вот это "определить разные методы [...] от условия [...] не можем" выглядит мягко говоря сомнительно. Может быть, стоило упомянуть про SFINAE и объяснить чем он здесь не подходит и чем предлагаемый подход лучше, вместо этого категорического "не можем"?

                        0
                        > Невозможность — это не когда «серьёзные препятствия», а когда возможности совсем нет, по крайней мере без серьёзного изменения исходных.

                        Может казаться, что я изворачиваюсь, но. Так уж получилось, что на C++ программирую довольно давно и время от времени сталкивался с ситуациями, когда компилятор просто падал на каком-то сложном коде. Т.е. код был формально правильным, успешно собирался другим компилятором, но вот нужный мне компилятор выпадал с ICE. И приходилось код переделывать, чтобы компилятор смог с ним справиться. В последний раз такое было в 2016-ом году, как раз там сложное сочетание шаблонов мы пытались задействовать.

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

                        > Но в процитированной выше фразе не шло речи об определениях новых полей класса. Только методов.

                        Здесь, видимо, плохо сформулированная фраза в тексте. К сожалению, не только код приходится писать в условиях горящих сроков, но и время на написание статей не резиновое… :(

                        Тут вы правы, безусловно.

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

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

                        PS. Я ваш комментарий не минусовал.
                      0
                      Не препирайтесь) И так и так получается так себе… )))
                        0

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

                  +2

                  Мне вот почему-то кажется что это не один класс-контейнер должен быть а целый набор РАЗНЫХ классов… Потому что ведут они себя по разному, хранят данные разного характера и т.п.


                  Если утка и самолёт оба летают это не делает их одной сущностью у которое «просто разные стратегии заправки и удержания себя в воздухе»


                  Поправьте если я не прав..

                    0

                    Исторически был только один класс сообщений — наследники от message_t, поэтому конструкция:


                    send<some_message>(dest, ...);

                    была вполне себе однозначной и не требовала какого-либо разнообразия при обработке типа some_message.


                    Но, со временем, потребовалось поддерживать варианты, когда some_message мог быть произвольным типом. Поэтому внутри SObjectizer-а пришлось различать ситуации, когда сообщение внутри представляется как intrusive_ptr_t<Msg>, а так же когда сообщение представляется как intrusive_ptr_t<user_type_message_t<Msg>>.


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


                    И возник вопрос, как сделать это так, чтобы пользователю не нужно было думать, хранить ли intrusive_ptr_t<some_message> или intrusive_ptr_t<user_type_message_t<some_message>>. В качестве решения был придуман класс message_holder_t<Msg>, который брал бы на себя все эти детали.


                    Изначально задумывалось, что message_holder_t<Msg> будет вести себя как shared_ptr для иммутабельных сообщений и как unique_ptr для мутабельных. Но даже на штатных примерах SObjectizer-а выяснилось, что такого простого разделения недостаточно. Как минимум нужно иметь логику shared_ptr для мутабельных сообщений. Отсюда и добавление параметра Ownership в шаблон message_holder_t.


                    Можно ли было покрыть все это разными типами?


                    Наверное да.


                    Но вот было бы это удобно для пользователя фреймворка?..


                    Вот в этом я лично сильно сомневаюсь. Особенно с учетом того, что агенты могут быть шаблонами. И внутри шаблона не так уж просто разбираться с тем, как хранить сообщение. Тогда как message_holder_t берет на себя все эти проблемы.

                      0

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

                        0

                        Так вы запросто можете получить те типы, которые вам нужны:


                        template<typename M>
                        using shared_message_holder_t = message_holder_t<T, message_ownership_t::shared>;
                        
                        template<typename M>
                        using unique_message_holder_t = message_holder_t<T, message_ownership_t::unique>;
                          0

                          Это понятно, что так можно делать. Я попробую перефразировать — мне кажется типы принципиально разные, поэтому ничего общего на уровне «интерфейса» они иметь не должны. Сам факт того что внешний (для вашей библиотеки) код пытается относится к этим, по сути, разным типам как к чему-то единому может указывать на проблемы дизайна и нестыковки в доменной модели (возможно я ошибаюсь — глубоко не вникал — но субъективное ощущение именно такое).


                          ...


                          Я несколько лет назад ушёл из C++ в C# и некоторое время мне было тяжело привыкнуть к «ограничениям» языка — множественное наследование — нельзя, шаблонная магия — нельзя, частичная специализация — нельзя и т.п. Потом, со временем, я понял что эти «ограничения» вынуждают меня писать более чистый код — в 99.9% случаев моя (провальная) попытка использовать что-то из запрещённых приёмов приводила меня к более простому и понятному коду в итоге.


                          ...


                          Конкретно в случае акторов и сообщений я бы попытался сделать сообщения простыми DTO объектами которые создаются в момент отправки и уничтожаются после доставки сообщения немедленно. Если кому-то нужно «сохранить» сообщение — пусть копирует его данные к себе. Таким образом формат сообщений не будет влиять на внутренности реализации потребителей — метод который вытаскивает из сообщения полезные данные и сохраняет (в другой тип!) будет в одном месте и его легко подправлять в случае чего. Ну и в целом по код станет понятнее и без шаблонной магии. Из минусов это может быть немного медленнее (надо мерять) и возможно чуть более размашистей по количеству строчек кода. Но в целом ИМХО будет проще.


                          p.s. Вышенаписанное здесь на правах альтернативного мнения и концептуальных идей. Я не вникал в библиотеку глубоко и могу в чём-то заблуждаться, но идея должна быть понятна: делайте проще.


                          p.p.s. Сам этим страдаю :(

                            0

                            В догонку: это иллюзия что теперь все хорошо клиенты могут использовать одинаково message_holder — его семантика скрыта в типе, концептуально, для разных типов, код будет разным даже если написать auto message = ...; выглядеть-то он будет одинаково, а вот поведение разное и его все равно нужно держать в голове.

                              0

                              Немного путано получилось, но надеюсь суть Вы уловили. Пишу с телефона — сори.

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

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


                              template<typename Request_Msg, typename Ack_Msg, typename Nack_Msg>
                              class my_actor final : public so_5::agent_t {...};

                              Если в таком шаблонном акторе нужно сохранить сообщение, скажем, типа Request_Msg, то гораздо проще это делать когда есть всего один message_holder_t, чем разные типы holder-ов.


                              Конкретно в случае акторов и сообщений я бы попытался сделать сообщения простыми DTO объектами которые создаются в момент отправки и уничтожаются после доставки сообщения немедленно.

                              Такое решение нужно было принимать лет 9 назад. А так как сообщения не DTO объекты, и летают не только в режиме 1:1, но и 1:M, то теперь приходится поддерживать то, что уже есть.

                                0

                                Ну мы кажется поняли друг друга :)


                                • см. мой второй комментарий — то что теперь можно писать шаблонные акторы принимающие разные типы и как-то их сохраняющие ИМХО не есть айс. Потому что семантика этих типов разная а в коде это никак не видно и приходится держать в голове.

                                По поводу 9 лет назад — может новую мажорную версию выпустить? :) 1: М имхо не есть проблема — главное явно уничтожить сообщение после того как все М отработают. Тогда всем придётся сообщение (точнее его данные) копировать если надо. Из бесплатных бонусов — невозможность неявного влияния одного актора на другой — то что вы сейчас закрываете иммутабельностью. Медленней чуток, да… но не факт что это проблема — сервера вон джейсоны гоняют и парсят направо-налево и ничего с ними страшного не происходит.

                                  0
                                  то что теперь можно писать шаблонные акторы принимающие разные типы и как-то их сохраняющие ИМХО не есть айс.

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


                                  По поводу 9 лет назад — может новую мажорную версию выпустить?

                                  Так версия 5.6, которая сейчас в фазе стабилизации находится, и есть мажорная. Почти 4.5 года мы развивали версию 5.5 с самой серьезной оглядкой на совместимость. В этом году нарушаем совместимость, очищаемся от старых костылей и выкатываем 5.6.


                                  Но слишком уж кардинально менять принципы работы — это не вариант.

                      –1
                      При решении какой реальной задачи возникла эта абстрактная задача?
                      В C++ очень любят создавать проблему из ничего и раздувать её, а потом героически преодолевать всё это в общем виде, при этом если чего-то не хватает добавлять это в стандарт и решать эту же задачу еще раз но «лучше».
                      При этом связь с реальной физической проблемой теряется, но такие мелочи никому не интересны.
                        +1
                        При решении какой реальной задачи возникла эта абстрактная задача?

                        Во-первых, задача не абстрактная.


                        Во-вторых, хейтеры C++ и свидетели секты "на-С-можно-сделать-проще" могут спокойно проходить мимо. Ну или поставить себе очередную зарубку "эти C++ники сами придумали себе проблему и героически ее решили". Ваше самомнение никто данной статьей поколебать не хотел.


                        Если вам всерьез интересно, зачем все это потребовалось, то, пожалуйста, вот обоснование:


                        Есть фреймворк, который позволяет пользователю строить свое прикладное решение на базе обмена сообщениями. При использовании этого фреймворка возникают три ситуации, когда некий актор (агент) должен манипулировать заранее созданным экземпляром сообщения:


                        1. Актор создается в некий момент T, когда есть все значения для создания экземпляра сообщения M. Агент должен сохранить у себя все эти значения и дождаться команды "ты можешь начать работать". Когда такая команда приходит, агент должен отослать куда-то сообщение M. Но команда может и не прийти. Возникает вопрос: как сохранить параметры для сообщения M? Можно их сохранить как атрибуты агента, а потом из атрибутов создать сообщение M. А можно сразу создать сообщение M и хранить его до момента времени Ч. Соответственно, message_holder_t решает задачу хранения M.
                        2. Некий агент получает сообщение M и должен сохранить его у себя до какого-то момента. Например, этот агент является балансировщиком нагрузки. Он получает сообщения, складирует их у себя и раздает по мере возможности агентам-воркерам. Возникает вопрос: как складировать эти сообщения M? Соответственно, message_holder_t решает эту проблему.
                        3. Бывает необходимость организовать обмен преаллоцированными сообщениями. Т.е. сообщения создаются при старте программы, только при старте выделяется память под сообщения, а потом используются только преаллоцированные сообщения. Возникает вопрос, как хранить эти преаллоцированные сообщения? Шаблон message_holder_t решает эту проблему.

                        Так что задача для пользователей конкретного фреймворка отнюдь не абстрактная.

                          –2
                          То есть решается проблема, созданная фреймворком инструментом написанным специально для решения «неких» проблем сферических коней в адиабатическом поле с голономными связями?
                            +1

                            И почему мне кажется, что вы сюда не для конструктивного общения пришли?


                            Мы сделали и развиваем инструмент, который упрощает многопоточное программирование. Кому-то он помогает. Кто-то в нем не нуждается.


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


                            Но, если не лень, поинтересуйтесь на досуге, почему разработчики и пользователи таких инструментов, как Akka, Orleans, Actix, CAF, QP/C, Celluloid и пр. не рассматривают это все как "проблемы сферических коней в адиабатическом поле с голономными связями".

                              –1
                              У вас наверное просто глаз замылен. После прочтения постановки задачи и ограничений на решение сразу видно что это именно борьба с мельницами которые сами же построили. Когда инструмент для упрощения создаёт больше проблем чем решает это повод задуматься.
                              Почему-то эрланге без static if обходятся.
                                0
                                > Почему-то эрланге без static if обходятся.

                                Там еще и без классов обходятся. И узкие места на C пишут. А потом вообще берут, и Elixir на базе Erlang-овской VM делают. Видимо потому, что Erlang настолько хорош.
                                  –2
                                  Так для этого языки склейки типа python, javascript, lua… и нужны что бы вынести языки типа C и C++ под капот и решать реальные задачи отделив людей от аппаратной части и надуманных проблем борьбы с дополнительной сложностью, возникшей в процессе эволюции инструмента призванного бороться со сложностью первоначальной задачи.
                                  Причем тут erlang-vm для узких мест при необходимости и специальное железо делают.
                                    +1

                                    Это у вас нужно спросить, причем тут Erlang. Вы же сказали:


                                    Почему-то эрланге без static if обходятся.

                                    В Erlang-е много без чего обходятся. И, что характерно, обходятся далеко не всегда. Поэтому, в частности, делают Elixir, чтобы получить больше возможностей в рамках экосистемы Erlang-а (ибо выразительной мощи Erlang-у для каких-то проектов явно не хватает).


                                    Что означает, что не следует оглядываться на Erlang, когда речь идет про разработку на C++.


                                    Далее, если я правильно понимаю вашу точку зрения, нет смысла писать все на C++. Мол, нужно на C++ написать только отдельные куски, для которых критична производительность, а все остальное — на Python, Ruby, JS и пр.


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


                                    Есть простой факт, который, возможно, вам непонятен или вызывает жжение в каких-то местах вашего организма, но тем не менее, люди пишут софт на C++. Иногда целиком, иногда какие-то изрядные куски софта. Но C++ широко используется для решения сложных и объемный (в смысле объема результирующего кода) задач.


                                    А раз так, то C++никам нужны различные инструменты, которые облегчают им их работу. Мы один из таких инструментов делаем. И, по мере сил рассказываем о том, что и как мы делаем. Что, как показывает практика, кому-то даже интересно. Данная статья — один из таких рассказиков.


                                    Если вам кажется, что это все борьба с искусственной сложностью, то вы спокойно можете продолжать так думать. Не вижу никакого смысла разубеждать вас в этом. Я рассказал, зачем это все может потребоваться. Если вас это не убедило — нет проблем. В мире еще столько всего странного, чего вы не понимаете и к чему вы можете докопаться. Так что нет смысла заострять внимание именно на нас.

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

                        Если сообщение неизменяемое (а по умолчанию оно неизменяемое), то методы-getter-ы должны возвращать константный указатель на сообщение. А если изменяемое, то getter-ы должны возвращать не константный указатель.
                        Если значение иммутабельно как тип, то какой указатель на него не верни, будет значение, которое не получится изменить. Если экземпляр иммутабельного типа можно нечаянно изменить, то это как минимум странно. Если тип на самом деле не иммутабелен, а просто хочется некоторые экземпляры сделать иммутабельными, то наверное это надо делать не силами контейнера?

                        Когда-то он должен вести себя как std::shared_ptr, т.е. можно иметь несколько message_holder-ов, ссылающихся на один и тот же экземпляр сообщения. А когда-то он должен вести себя как std::unique_ptr, т.е. только один экземпляр message_holder-а может ссылаться на экземпляр сообщения.
                        Эти стратегии владения настолько разные, что лучше и типа иметь два разных. Иначе получается странный тип, поведение которого прояснится только после (не)успешной компиляции. Ну а чтобы пользователь не расслаблялся в случае успешной компиляции,
                        А метод make_reference должен изымать указатель у объекта message_holder_t: после вызова make_reference исходный message_holder_t должен остаться пустым.
                        перенесем один из сюрпризов на время выполнения =). Это что угодно, но только не «make a reference», не надо так =/.

                        Кстати, выключать конструкторы-операторы можно еще и таким образом, без применения дополнительного наследования:
                        template<typename T> struct Foo {
                            Foo() { }
                            Foo(const Foo&) {
                                static_assert(std::is_integral<T>::value, "");
                            }
                        };
                        
                        void test() {
                            Foo<int> f1;
                            Foo<int> f2{f1}; // Ok
                            Foo<std::string> f3;
                            Foo<std::string> f4{f3}; // Fail
                        }
                        

                        Поправьте меня, если я неправ, но вроде бы по стандарту методы шаблона инстанцируются только если они используются. Ну, как минимум все крупные компиляторы ведут себя желаемым образом =).
                          0
                          На мой взгляд в статье недостаточно проиллюстрированы озвученные требования к типу-обертке.

                          Старался описать максимально подробно и без лишнего погружения в детали. Не знаю, как описать еще проще. Если все равно остались вопросы, то задавайте. Вероятно, проще будет ответить на них.


                          Если значение иммутабельно как тип, то какой указатель на него не верни, будет значение, которое не получится изменить.

                          Так в этом и особенность задачи: если сообщение иммутабельное, то хоть в случае поведения shared_ptr, хоть в случае поведения unique_ptr, возвращаться всегда должен константный указатель на содержимое сообщения.


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

                          Да, разные. Но причины иметь единственный шаблон message_holder_t описаны здесь: https://habr.com/ru/post/449122/#comment_20067302


                          Это что угодно, но только не «make a reference», не надо так =/.

                          Это дань историческим традициям.


                          Кстати, выключать конструкторы-операторы можно еще и таким образом, без применения дополнительного наследования:

                          В случае, если два метода следует исключить из рассмотрения компилятора, условия в static_assert пришлось бы продублировать. Если таких методов три — то нужно трехкратное дублирование и т.д.

                          0
                          Статья хорошая и описывает трудности, которые реально нет-нет да и да. Предложенное решение вроде работает, но, на мой вкус, не производит впечатления простоты. Что касается D и static if как вариант условной компиляции на стероидах, то, опять же, с моей скромной точки зрения, это может работать в простых случаях, но при усложнении заведет в тупик, так же как enable_if/SFINAE, if constexpr и т.д. Все эти решения, ИМХО, плохо масштабируются при росте сложности и плохо применимы за достаточно узкими пределами.
                          Лучшим решением, на мой взгляд, тут была бы наследование и dynamic_cast. При всех минусах этого подхода.
                            0
                            > Предложенное решение вроде работает, но, на мой вкус, не производит впечатления простоты.

                            Статья как раз и была написана для того, чтобы показать, что простоты здесь и нет. Не смотря на то, что я сам не люблю сложных решений и пытался обойтись минимальными наворотами.
                              +1
                              Я, видимо, неточно выразился. Нормальное библиотечное решение. Еще бы закрыть практическое использование макросом, чтобы меньше печатать — и в продакшен). Под сложностью я понимаю следующее:
                              1. Могу ли я понять Вашу статью. Громоздко — да. Но понять не сложно.
                              2. Понял бы я «шоетазанах», столкнувшись с таким кодом без комментариев? Ну, не знаю, если бы с использованием не было бы проблем, то громоздкость меня бы отпугнула.
                              3. Смог бы я до прочтения Вашей статьи сам такое написать? Не факт, сильно сомневаюсь.

                              Могу только согласиться с тем, что C++ шаблонное метапрограммирование несовершенно и слишком усложняется при усложнении задачи. Не уверен, но именно для решения подобных Вашим проблем Саттер и предлагает метаклассы.
                              Что не понравилось — Вы хотели бы изменить сигнатуры типов с помощью условной компиляции, чтобы меньше печатать и легче читать. Понятно, но я считаю применение условной компиляции для таких задач принципиально ошибочным.
                                0
                                Что не понравилось — Вы хотели бы изменить сигнатуры типов с помощью условной компиляции, чтобы меньше печатать и легче читать.

                                Тут нужно дать некоторое пояснение. Когда я только начал делать этот самый message_holder_t, то предполагалось, что различия в составе и сигнатурах методов, в зависимости от параметров шаблона, будут более существенными. Но по мере реализации стало выясняться, что этого не произойдет. Что разница будет на уровне сигнатур методов. Более того, уже по ходу написания статьи пришло понимание того, как обойтись одним msg_accessor_t, а не двумя, как в том варианте, который и пошел в работу.


                                Так что, когда статья была завершена, выяснилось, что различия в вариантах кода не столь существенны и наглядны. Но и отказаться от публикации не смог :)


                                Теперь пару слов про субъективность восприятия.


                                ИМХО, это нормально, когда мне нравится один подход, вам другой, кому-то третий. Ведь это то, с чем мы постоянно имеем дело в жизни: дай трем разным людям, с разным опытом и взглядом на программирование одну и ту же задачу, они решат ее тремя разными способами. Причем не факт, что будут положительно отзываться о чужих решениях ;)


                                Я сделал тот вариант, который смог придумать (не факт, что лучший). Но при этом мне не понравилось то, что пришлось так выкручиваться (а другие варианты, которые я видел и которые даже здесь в комментариях озвучивались, мне казались еще хуже). При том, что я считаю подход на базе D-шного static if-а наиболее простым, прямолинейным и понятным для такого рода задачи. Посему очень жаль, что в C++ у меня такой возможности нет.


                                Это сожаление и явилось причиной, толкнувшей на написание статьи.

                            0

                            Я правильно понимаю что главный посыл статьи: "специализация шаблонного класса муторно, enable_if для включения/выключения функций класса не читабельно (?), условное наследование реализаций как у нас — сложно в поддержке"? То есть вам даны три инструмента (кстати на c++98/03 не сильно то отличаться реализация будет), но все равно нужен четвертый "как в D"?

                              0
                              По мне, сам факт того, что даны уже три инструмента для решения одной и той же задачи — это маркер того, что инструменты как-то не очень. Ну а там, где три, почему бы и не четыре?

                              Тем более, что не факт, что наличие `static if` не сделало бы ненужным два из этих трех инструментов.

                              > enable_if для включения/выключения функций класса не читабельно (?)

                              Мало того, что нечитабельно, так еще и ограничено в возможностях. Методы добавлять/изымать можно, а вот как быть с полями класса?
                                0
                                Еще раз скажу: вы хотите условной компиляции, только не на этапе препроцессирования, а на этапе инстанциации шаблонов. Как практик я Вас понимаю. Но в статье это выглядит, как пропаганда «срезания углов» в ущерб системе типов. Это всего лишь мое мнение, no obligations.
                                Но Ваша статья заставила меня еще раз присмотреться к идее метаклассов. Может быть, это и не так плохо, как мне казалось…
                                  0
                                  > Еще раз скажу: вы хотите условной компиляции, только не на этапе препроцессирования, а на этапе инстанциации шаблонов.

                                  У меня даже есть подозрение, что `static if` мог бы быть полезен и при определении обычных классов, а только не шаблонов.

                                  Т.е. суть в том, что в рамках чистого C отдельно препроцессор и отдельно язык вполне себе нормально дополняли друг друга (ибо оба были весьма убоги и ограничены). Но уже в C++ тот факт, что препроцессор вообще никак не связан с языком, уже гораздо печальнее. И подход к «условной компиляции», который сделали в D (а это не только static if, но еще и version, и static_foreach) выглядит гораздо лучше, чем то, что мы имеем в C++. А с учетом того, что в C++20 обещают завести модули и препроцессор там перейдет в категорию инструмента, который не очень-то и рекомендуется использовать, D-шные конструкции начинают выглядеть еще привлекательнее. ИМХО.
                                  0
                                  C++ в принципе такой язык что на каждую задачу найдется много решений, в этом его и прелесть в том числе. Мое мнение что «не читабельно» — это вкусовщина и тем более для библиотеки не очень важно.

                                  Странно полагаться на то что кто-то будет разбираться как там вы свои классы понаписали с условной компиляцией, будь там хоть эти три решения что уже есть в языке, хоть 'static if'. Программист-пользователь вашей библиотеки будет в первую очередь смотреть примеры, во вторую — документацию. А вот если ему придется лезть в чужой код библиотеки, да еще и шаблонный, да еще и с условной компиляцией, да еще и с такими условиями что классы ведут себя практически принципиально по-разному, то в большинстве случаев программист-пользователь выберет другое решение для своей задачи.

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

                                    Я на это смотрю так: SFINAE выглядит как побочный продукт развития шаблонов. Как то самое метапрограммирование на тех самых шаблонах: о нем не думали изначально, как бы само получилось. И как побочный продукт SFINAE быстро показывает свои ограничения. Т.е. когда есть одна функция, для которой нужно сделать перегрузки для разных ситуаций, SFINAE еще нормален. Да и то… Вот, скажем, фрагмент, который был написан прямо сегодня:


                                    template<
                                        typename Request,
                                        typename Reply,
                                        typename Target,
                                        typename Duration,
                                        typename... Args >
                                    [[nodiscard]]
                                    std::enable_if_t<
                                        std::is_default_constructible_v<
                                            typename request_reply_t<Request, Reply>::reply_t>,
                                        typename request_reply_t<Request, Reply>::reply_t>
                                    request_value(Target && target, Duration duration, Args && ...args )
                                        {
                                            using requester_type = request_reply_t<Request, Reply>;
                                            ...
                                        }
                                    
                                    template<
                                        typename Request,
                                        typename Reply,
                                        typename Target,
                                        typename Duration,
                                        typename... Args >
                                    [[nodiscard]]
                                    std::enable_if_t<
                                        !std::is_default_constructible_v<
                                            typename request_reply_t<Request, Reply>::reply_t>,
                                        typename request_reply_t<Request, Reply>::reply_t >
                                    request_value(
                                        Target && target,
                                        Duration duration,
                                        Args && ...args )
                                        {...}

                                    Уже здесь видно дублирование одного и того же. И, честно говоря, получившийся вариант мне сильно не нравится, но пока еще не придумалось как это исправить.


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


                                    При этом сейчас, в C++17, можно сделать и по другому. Вместо двух функций с разными предикатами в enable_if можно сделать всего одну, у которой внутри будет if constexpr:


                                    template<
                                        typename Request,
                                        typename Reply,
                                        typename Target,
                                        typename Duration,
                                        typename... Args >
                                    [[nodiscard]]
                                    auto
                                    request_value(Target && target, Duration duration, Args && ...args )
                                        {
                                            using requester_type = request_reply_t<Request, Reply>;
                                            if constexpr(std::is_default_constructible_v<
                                                typename requester_type::reply_t>)
                                            {
                                                ... // Простыня текста.
                                            }
                                            else
                                            {
                                                ... // Простыня текста.
                                            }
                                        }

                                    Т.е. уже сейчас SFINAE может быть заменен на if constexpr. А если бы if constexpr был еще мощнее, то во многих случаях от многословных и не всегда понятных SFINAE можно было бы вообще отказаться.


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

                                    Так в принципе уже сейчас если заглянуть в навороченную шаблонную библиотеку, в которой еще нет if constexpr вообще, есть хороший шанс утонуть в деталях реализации. И далеко не всякий C++ разработчик способен разобраться в чужом шаблонном коде, к сожалению.


                                    Я нахожусь при мнении, что мощный аналог static if в C++ был бы полезен, чем SFINAE. И, имхо, разработчики на D подтвердят, то это удобная штука.

                                      +1
                                      Так вот я с вами согласен и разделяю всю боль шаблонного программирования. Но диалог как-то странно у нас с вами строится:
                                      — Имеющиеся подходы к условной компиляции не удобны. Клево было бы иметь 'static if'.
                                      — Согласен, это клевая штука, но очень далеко не приоритетная и нужна по сути разработчику библиотеки, а не ее пользователю. Да и есть уже три подхода для решения таких задач в языке прямо сейчас.
                                      — Но вот же какой некрасивый код у разработчика библиотеки с такими подходами, а с 'static if' было бы не так страшно!
                                      — Согласен, код не очень читаемый для неподготовленного пользователя библиотеки, хотя задачу свою выполняет. Но его никто кроме разработчика библиотеки и смотреть не должен, все должно быть интуитивно, так же в примерах, и напоследок в документации.
                                      — Жаль что не каждый разработчик способен разобраться в шаблонном коде чужой библиотеки.
                                      — Так ведь это… не его задача — разбираться в чужом коде…
                                      — Мощный аналог 'static if' в C++ был бы полезен.
                                      — Так я же согласен… но не приоритетно… да и есть уже три подхода…
                                      — В D круто сделано, там разработчикам проще от этого.
                                      — Так согласен… Так крутая фишка, да… но ведь… ай ладно.

                                      Утрирую конечно, но как то так. =)
                                        0
                                        Да, диалог странный и вы хорошо его резюмировали :)

                                        Я вот не согласен с тем, что разработчику не нужно разбираться в чужом коде. Хорошо, когда не нужно это делать. Но когда приходится, то хорошо бы делать это без особых затрат. А если в чужой код залезть тяжело, то можно нарваться на «закон текущих абстракций».
                                          0
                                          Хорошо бы, да обычно «без затрат» — это взять и задать вопрос разработчику кода или вообще взять другое решение, благо их хватает обычно.

                                          Потому что «текущие абстракции» если это, к примеру, про просадки в производительности в чужом коде — решение не разобраться что же там понаписали то внутри, решение это пользоваться кодом по документации другим способом (очевидно более производительным) и как максимум завести issue в репозитории разработчика, либо перейти на другую библиотеку (другой код) и забыть об этой «тормознутой» библиотеке на время.

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

                                          Все это IMHO конечно, вопрос думаю дискуссионный и вряд ли мы придем к какому-то общему мнению тут при разных точках зрения изначально.
                                            0

                                            Просто немного дополню.


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


                                            Ну и бывает так, что библиотека настолько критична, что отказаться от нее крайне сложно. Например, если вначале разработки заложились на Asio, то по ходу дела сменить ее на libuv или ACE будет затруднительно, уж слишком по разному нужно писать код. Тоже самое можно сказать и о нашем SObjectizer-е и его прямых конкурентах в мире C++: CAF и QP/C++. Если уж конкретная библиотека для работы с акторами была выбрана и было написано несколько десятков тысяч строк кода с их использованием, то взять и заменить CAF на SObjectizer или SObjectizer на QP/C++ будет ну очень больно.

                                0
                                1. Нужно писать пропозал на const-operator, default-operator и delete-operator на манер noexcept-operator (чтобы можно было делать Foo() = default(someCompileTimeCheck)). Вот это бы было в стиле плюсов!
                                  Потом можно на override и на virtual, ну, так, для полноты. И на && ещё, auto getFoo() &&(check) — хорошо, ящетаю.
                                2. А зачем вы явно пишете конструкторы структурам типа user_message? Я как пользователь сильно расстроюсь, ведь этим вы мне запретили использовать самую полезную фичу из C++20 — designated initializers, которые, впрочем, уже давно поддерживаются gcc и clang.
                                  0
                                  А зачем вы явно пишете конструкторы структурам типа user_message?

                                  Здесь user_message — это прямая копипаста с so5_message, из которой было удалено ненужное наследование, все остальное осталось.


                                  Я как пользователь сильно расстроюсь, ведь этим вы мне запретили использовать самую полезную фичу из C++20 — designated initializers, которые, впрочем, уже давно поддерживаются gcc и clang.

                                  Для нас и VC++ является компилятором, который в обязательном порядке нужно поддерживать. А там, в VS2017 еще не было вроде designated initializers. VS2019 пока еще не пробовал.


                                  Кроме того, в случае с user_type_message пользователю не приходится создавать экземпляры вручную. Это делается автоматически либо через send, либо описанный message_holder_t, внутри которых вызывается конструктор пользовательского сообщения, в который аргументы передаются через variadic templates.

                                  0
                                  Не хочу показаться снобом, но где магия? Автор решил простую задачу стандартными средствами.
                                  Кроме того, решение автора (хоть я и кое-что поправил бы) выглядит гораздо лучше чем портянка кода, где в объявлении класса тебе приходится разбираться со всякими if'ами.
                                    0
                                    Не хочу показаться снобом, но где магия?

                                    Так в этой статье вроде бы магия и не обещалась.


                                    хоть я и кое-что поправил бы

                                    Что, если не секрет?


                                    выглядит гораздо лучше чем портянка кода, где в объявлении класса тебе приходится разбираться со всякими if'ами.

                                    Фокус в том, что когда стало понятно, что нужен этот самый message_holder_t, то его черновой вариант на static if-ах был набросан где-то минут за 15. Потом в течении нескольких часов из варианта на static if-ах создавался вариант на базовых типах и метафункциях. И этот вариант затем в течении двух последующих дней видоизменялся и упрощался.


                                    15 минут и пара-тройка часов.


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

                                      0
                                      Так в этой статье вроде бы магия и не обещалась.

                                      Ну, упоминание шаблонной магии есть.

                                      Что, если не секрет?

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

                                      Понятно, что главная проблема в том, что я уже просто старый и тупой

                                      Скорее в отсутствии опыта. Если бы Вы перед решением этой задачи пару недель ковырялись в метапрограммировании, ответ пришёл бы быстрее и проще.
                                      А то, что Вы хотите проще, быстрее и приседаний поменьше — вполне логично и ожидаемо, только вот на мой взгляд это неправильный выбор. Это как хотеть наклепать одну большую глобальную функцию, вместо того, чтобы разбивать задачу на более мелкие и подбирать удачные структуры данных.
                                        0
                                        Ну, упоминание шаблонной магии есть.

                                        Как и слова о том, что углубляться в уже существующую магию не будем.


                                        Я бы все наследуемые классы, которые получаются после вычисления мета-функций, вынес бы в аргументы шаблона для более цельного восприятия.

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


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

                                        Тут почему-то приходят мысли на счет стокгольмского синдрома: если привыкнуть одевать штаны через голову, то данная задача становится привычной. Нужно ли привыкать ;)

                                          0
                                          Тут не понятно. Каждая метафункция получает столько аргументов, сколько нужно, не больше, не меньше. И возвращает всего один тип. Мне сложно представить, куда и затем этот тип еще нужно передавать.

                                          Я примерно об этом:
                                          код
                                          template<
                                             typename Msg,
                                             message_ownership_t Ownership = message_ownership_t::autodetected >,
                                             typename Inherited = details::message_holder_details::accessor_selector_t<
                                                      details::message_mutability_traits<Msg>::mutability,
                                                      details::message_holder_details::impl_selector_t<Msg, Ownership> >,
                                             typename Base_type = details::message_holder_details::accessor_selector_t<
                                                      details::message_mutability_traits<Msg>::mutability,
                                                      details::message_holder_details::impl_selector_t<Msg, Ownership> > >
                                          class message_holder_t : public Inherited
                                          {
                                          ...
                                          



                                          Тут почему-то приходят мысли на счет стокгольмского синдрома: если привыкнуть одевать штаны через голову, то данная задача становится привычной. Нужно ли привыкать ;)


                                          Неправда, if'ы в объявлении класса мало того, что выглядят чужеродно, так ещё и создают путаницу. То, что это решение проще и реализуется за минуты, не значит, что оно лучше и будет хорошо работать потом. Банальный пример: понадобится усложнить критерии выбора наследуемых типов — Вы будете в классе уже каскады if'ов делать? Объявление тогда превратится в какой-то рабочий код.
                                            0
                                            > Я примерно об этом:

                                            Если бы я ревьювил такой код, я бы его отклонил. Поскольку:

                                            1. Определение шаблона становится слишком сложным.
                                            2. В интерфейс шаблона (а список его параметров — это интерфейс) закладываются не нужные пользователю детали реализации.

                                            > Неправда, if'ы в объявлении класса мало того, что выглядят чужеродно

                                            Для человека, который приходит в C++ с других языков, фокусы с SFINAE или специализацией шаблонов так же выглядят чужеродно. Так что тут дело привычки. C++ники не привыкли к static if, а D-шники — привыкли. В Ruby, например, так же можно if в определении класса использовать, и это применяют на практике.

                                            > Банальный пример: понадобится усложнить критерии выбора наследуемых типов — Вы будете в классе уже каскады if'ов делать?

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

                                              Какая разница: слишком сложно определение класса или шаблона? В любом случае будет много кода, но так хотя бы все параметры в одном месте.
                                              В интерфейс шаблона (а список его параметров — это интерфейс) закладываются не нужные пользователю детали реализации.

                                              Это легко решается алиасом.
                                              В Ruby, например, так же можно if в определении класса использовать, и это применяют на практике.

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

                                              Логика по выбору классов для наследования будет вынесена в отдельную метафункцию, что делает изменение простым и понятным. Скорее всего, надо будет подправить эту метафункцию и добавить необходимые классы для наследования.
                                                0
                                                Какая разница: слишком сложно определение класса или шаблона?

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


                                                В любом случае будет много кода, но так хотя бы все параметры в одном месте.

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


                                                Это легко решается алиасом.

                                                Т.е. мы добавляем лишнюю сущность в код для того, чтобы спрятать параметр, который в принципе не нужен. И который уж точно не должен быть виден пользователю (т.к. ничто не помешает пользователю по недосмотру подставить собственное значение в этот параметр).


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

                                                Так давайте посмотрим на историю развития C++ вообще. Сперва C++ позаимствовал идеи и некоторые ключевые слова из Simula, а синтаксис и базовую stdlib из C. Потом в C++ добавили шаблоны и исключения, которых изначально в C++ не было, но они были в других языках, в частности в ML и Ada. STL пришел в C++ из результатов работ для Scheme и Ada. Затем в C++ добавили лямбда-функции. Которые к тому времени были практически везде, кроме C++. Потом в C++ добавили возможность вычислять функции во время компиляции, а данная возможность уже была к тому времени в D. Из того же D подтянули if constexpr. Сейчас в C++ завозят co_async/co_await (которые уже есть в некоторых мейнстримовых языках) и контракты (которые уже более 30 лет есть в Eiffel, которые давно есть в D и не так давно в Ada). Ведутся работы над рефлексией, которая в других языках есть уже очень и очень давно (в том числе и compile-time рефлексия в том же D).


                                                Так что C++ всегда вбирал в себя идеи из других языков, вбирает и будет вбирать — это нормальный процесс эволюции применяемого на практике инструмента.


                                                И если какая-то вкусность способна сделать C++ более простым в использовании, то почему бы и нет?


                                                Логика по выбору классов для наследования будет вынесена в отдельную метафункцию, что делает изменение простым и понятным.

                                                Тоже самое можно сказать и по поводу предиката для static if.


                                                Скорее всего, надо будет подправить эту метафункцию и добавить необходимые классы для наследования.

                                                Проблема в том, что в отсутствии static if эти самые классы нужно придумать: сколько их, кто за что отвечает, как они соотносятся друг с другом, в каком порядке образуют цепочку наследования.


                                                Это сложно для многих C++ разработчиков. Тогда как static if может все эти заботы сделать ненужными.

                                                  0
                                                  Существенная, т.к. в реализацию заглядывать придется немногим, а параметры шаблона будут торчать наружу всегда. В том числе и в сообщениях об ошибках компилятора.

                                                  В чём будет проявляться их «торчание», кроме ошибок компилера? И в этих самых ошибках я проблемы не вижу.
                                                  Мне не понятно, какая выгода от того, чтобы результат метафункции засовывать в параметр шаблона.

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

                                                  Не вижу проблемы. Как Вы и сказали, «в реализацию придётся заглядывать немногим».
                                                  И если какая-то вкусность способна сделать C++ более простым в использовании, то почему бы и нет?

                                                  Не знаю на счёт истории, но сейчас подобные конструкции явно лишние в языке. Заимствования, которые Вы описали, отторжения не вызывают, потому что они не входят в конфликт с текущим синтаксисом.
                                                  Что в таком случае мешает использовать макросы? Просто же сразу станет. По-моему простота — это не то, за чем стоит безусловно гнаться.
                                                  Тоже самое можно сказать и по поводу предиката для static if.

                                                  Каким образом? Куда Вы вложенные if'ы спрячете?
                                                  Проблема в том, что в отсутствии static if эти самые классы нужно придумать

                                                  Не вижу проблемы. Делать классы-франкентшейны не должно быть легко и быстро.
                                                  Это сложно для многих C++ разработчиков.

                                                  Если программисту сложно (именно сложно, а не геморно) решить задачу на C++, то, возможно, стоит использовать другой язык? Для меня, например, сложно писать код в функциональном стиле, но я же не предлагаю из-за этого добавить простые человеческие циклы в haskell.
                                                  Тогда как static if может все эти заботы сделать ненужными.

                                                  И добавит других забот и проблем.
                                                    0
                                                    В чём будет проявляться их «торчание», кроме ошибок компилера?

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


                                                    У Вас определение класса зависит от каких-то типов. А это, фактически, и есть определение шаблонов.

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


                                                    А есть детали реализации шаблона. Эти и на эти детали влияет то, что пользователь задает в параметрах шаблона. Поэтому как-то странно выставлять детали реализации наружу и оформлять их в виде публично доступных параметров.


                                                    Не вижу проблемы.

                                                    Так ведь лишняя сущность. Если без нее можно обойтись, значит без нее следует обойтись. Ведь каждая сущность в коде увеличивает стоимость последующего сопровождения.


                                                    Каким образом? Куда Вы вложенные if'ы спрячете?

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


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


                                                    Делать классы-франкентшейны не должно быть легко и быстро.

                                                    Как по мне, так это цель любого практичного языка: дать разработчику возможность делать легко и быстро то, что разработчику нужно. Как это говорил сам Страуструп: "Простые задачи должны решаться просто, а решение сложных должно быть возможно". Если в простых случаях static if окажется сильно проще условного наследования, то это же хорошо. А если в результате получается класс-франкенштейн — то это уже проблема другого порядка. Гораздо хуже, когда класс-франкенштейн нужен, а его не сделать.


                                                    Если программисту сложно (именно сложно, а не геморно) решить задачу на C++, то, возможно, стоит использовать другой язык?

                                                    Так ведь C++ и так уже слишком сложный. И порог входа в него для новичков слишком высок. Это одна из причин, по которой от C++ отказываются в новых проектах даже там, где по техническим причинам C++ мог бы быть хорошим выбором. Так что, имхо, нужно не советовать брать другой язык тем, кому возиться с особенностями C++ сложно. А упрощать C++, чтобы порог входа был ниже.


                                                    Насколько снизит порог входа static if — это отдельный вопрос. Как по мне, так снизит. Но тут кроме ИМХО доказать нечем.

                                                      0
                                                      Так ведь лишняя сущность. Если без нее можно обойтись, значит без нее следует обойтись

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

                                                      Спасибо, не надо. Ещё не хватало в определениях классов голову в условиях ломать.
                                                      Если в простых случаях static if окажется сильно проще условного наследования, то это же хорошо.

                                                      Мы все прекрасно понимаем, что очень скоро «static if» окажется далеко не только в простых случаях. «Стрелял себе в ногу, попал в компилятор, ранены все».
                                                      Так ведь C++ и так уже слишком сложный.

                                                      Он не сложный, а объёмный. Но достаточно гармоничный пока что.
                                                      И порог входа в него для новичков слишком высок.

                                                      И это не правда. Для того, чтобы писать простые программы, каких-то особых знаний вообще не требуется. Другое дело, что многие воспринимают плюсы как «сложный питон (джаваскрипт, пхп)» и пытаются писать программы так же. Естественно, это вызывает искреннее непонимание у компилятора.
                                                      Это одна из причин, по которой от C++ отказываются в новых проектах даже там, где по техническим причинам C++ мог бы быть хорошим выбором.

                                                      Новички делают какие-то проекты, и не выбирают C++? Индустрия это переживёт, я думаю. А компании обычно способны нанять программиста, специализирущегося на C++.
                                                      А упрощать C++, чтобы порог входа был ниже.

                                                      Не стоит. Не надо делать из C++ то, чем он не является.
                                                        0
                                                        Ценой разбросанных по всему классу метафункций.

                                                        Они не разбросаны.


                                                        Ещё не хватало в определениях классов голову в условиях ломать.

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


                                                        Мы все прекрасно понимаем, что очень скоро «static if» окажется далеко не только в простых случаях.

                                                        Зато сейчас условное наследование даже в простых случаях. И это showstopper для многих C++ников.


                                                        И это не правда.

                                                        К сожалению, правда.


                                                        Новички делают какие-то проекты, и не выбирают C++?

                                                        Нет. Старички предпочитают выбирать Go, Rust, C#, Scala, Kotlin и т.д. C++ продолжают использовать крупные компании, у которых большущий legacy и уже приличный штат C++ разработчиков. В мелких компаниях уже не так.

                                                          0
                                                          Евгений, ну как Вам не ай-я-яй))) Ну все уже понимают, что Вы правы в практическом смысле: в паре мест впихнуть static if и не заморачиваться. В квалифицированной команде все все поймут правильно и хрен с ней, к системой типов, не на хаскеле пишем…
                                                            0
                                                            Ok, закругляюсь :)
                                                            0
                                                            К сожалению, правда.

                                                            В чём принципиальная сложность плюсов?
                                                            Нет. Старички предпочитают выбирать Go, Rust, C#, Scala, Kotlin и т.д.

                                                            Так всё правильно же. Сейчас для большинства задач с практической точки зрения подходят лучше те языки, которые Вы перечислили.
                                                              0
                                                              > В чём принципиальная сложность плюсов?

                                                              На такой вопрос в комментарии не ответишь.

                                                              > Сейчас для большинства задач с практической точки зрения подходят лучше те языки, которые Вы перечислили.

                                                              И какую долю в этом составляет сложность C++? Почему же эти языки лучше с практической точки зрения?
                                                                0
                                                                И какую долю в этом составляет сложность C++?

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

                                                                Почему же эти языки лучше с практической точки зрения?

                                                                Потому что они создавались для целей эффективного написания кода для распространённых задач (бизнес-логика, пользовательские приложения)? В отличие от C++ (или C), в который закладывалась возможность напрямую обращаться к памяти.
                                                                Только Rust тут в стороне, как наследник C++, но я что-то не видел, чтобы им активно пользовался enterprise.
                                                                  0
                                                                  У Вас же, насколько я понял, сложность языка — понятие исключительно практическое;

                                                                  Да, практическое. Поэтому сложность языка для меня — это, грубо говоря, количество времени и усилий, которые нужно потратить на изучение существующего кода и на написание нового кода. И вот с этим в C++, по разным причинам (даже краткий перечень которых не уместиться в комментарий) не очень хорошо. Например, если программист увидит код вида:


                                                                  int main() {
                                                                     device("0030:4568:0012")->turn_on(); // (1)
                                                                     ...
                                                                  }

                                                                  То сколько усилий ему потребуется, чтобы понять, что в действительности из себя представляет строка (1)?


                                                                  Даже если не брать в рассмотрение макросы, которые иногда пишут в нижнем регистре, то device может быть функцией. Например:


                                                                  something * device(const char * name);

                                                                  А может быть и такой:


                                                                  something * device(const std::string & name);

                                                                  А может быть и такой:


                                                                  something device(const device_name & name);

                                                                  И у каждого из вариантов есть свои скрытые накладные расходы и потенциальные точки для возникновения исключений. Более того, если это функция из первого варианта, то возвращает ли она указатель на динамически созданный объект или нет? (Может она написана 25 лет назад, когда мало кто заморачивался умными указателями).


                                                                  А может device — это класс, у которого конструктор принимает std::string, а так же перегружен operator->?


                                                                  Это всего лишь маленький пример по поводу понимания уже написанного кода. Но есть еще, к счастью, задача написания нового кода. И здесь легко вспомнить моменты, которые в C++ сложно назвать интуитивно понятными. Скажем, тот же SFINAE. Сильно сомневаюсь, что 99% изучающих C++ способно самостоятельно додуматься до такого трюка. Или другой пример: проверка наличия определенного метода у объекта или у класса. Дайте человеку, который никогда такого не делал в C++ эту задачу и запретите пользоваться Интернетом, дайте ему только пару-тройку книг по C++ и документацию по stdlib. Сколько времени займет ее решение и каковы шансы, что решение вообще будет найдено?


                                                                  Потому что они создавались для целей эффективного написания кода для распространённых задач (бизнес-логика, пользовательские приложения)?

                                                                  Я вроде специально оговорился, что от C++ сейчас, бывает, отказываются даже в тех нишах, где есть все предпосылки к использованию именно C++: работа с сетью и достаточно низким уровнем, обработка больших объемов данных, тяжелые вычисления, написание кода, который должен работать как на дестктопе, так и на мобилках, так и на умных гаджетах...


                                                                  Тому есть ряд причин, имхо. И среди них не на последнем месте находится соотношение между выгодой от использования C++ и проблемами, которые C++ привносит (небезопасность, сложность, время разработки, сложность поиска квалифицированных разработчиков). Сложность языка, о которой речь шла выше, здесь играет ведущую роль.

                                                                    0
                                                                    Это всего лишь маленький пример по поводу понимания уже написанного кода.

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

                                                                    Скажем, тот же SFINAE.

                                                                    А эта техника и не должна использоваться на каждом шагу. Как и эта:
                                                                    Или другой пример: проверка наличия определенного метода у объекта или у класса.

                                                                    Вы всё упорно пытаетесь сделать из C++ язык для быстрого написания кода высокого уровня и удивляетесь, что это не очень удобно. Странная позиция.

                                                                    работа с сетью

                                                                    По-моему работа с сетью никогда не была коньком плюсов. В стандартной либе до сих пор нет ничего для работы с сетью.

                                                                    достаточно низким уровнем

                                                                    Эта работа с достаточно низким уровнем должна вестись во всём проекте, или нужно пару оптимизаций, а всё остальное — обычное приложение? Просто я с трудом представляю себе целый проект, который целиком про ковыряние в регистрах, но при этом его пишут на каком-нибудь C#.

                                                                    обработка больших объемов данных

                                                                    С этим успешно справляются многие другие языки, в которые были добавлены соответствующие диалекты или оптимизации на фоне бурного роста популярности больших данных.

                                                                    написание кода, который должен работать как на дестктопе, так и на мобилках, так и на умных гаджетах

                                                                    Вот это вообще не ниша плюсов. Кроссплатформенность — это к java и иже.

                                                                    Тому есть ряд причин, имхо.

                                                                    Имхо просто не стоит гвозди забивать микроскопом.
                                                                      0
                                                                      > Не понял что Вы этим хотели сказать.

                                                                      То, что в C++ за одной строчкой может скрываться столько внутренней механики, что разработчикам на других языках может даже и не сниться. А с учетом того, что C++ небезопасен и не прощает ошибок, во всю эту механику приходится плотно погружаться. В безопасных языках ситуация другая: там если ошибся вылетит исключение в run-time и можно будет определить где, что и почему.

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

                                                                      Могу ошибаться, но на таких языках, как Go, Eiffel, Ada, Java это сделать гораздо сложнее.

                                                                      > По-моему это вообще неважно, ибо функция должна быть либо документирована, либо написана так, чтобы ничего не сломать в любом случае.

                                                                      Нет, проблема не в наличии или качестве документации. Проблема в том, что глядя на код сложно себе представить что на самом деле здесь происходит (вызов функции или конструктора, количество неявных преобразований).

                                                                      > Вы всё упорно пытаетесь сделать из C++ язык для быстрого написания кода высокого уровня и удивляетесь, что это не очень удобно. Странная позиция.

                                                                      Между тем, C++ вполне позволяет такое делать. И вопрос упирается лишь в наличие библиотек, заточенных под вашу прикладную нишу. Моя позиция определяется тем, что написание таких библиотек в C++ является делом непростым. Сильно сложнее, чем в Go, Rust-е, С, Java и т.д. И вот все эти фокусы с SFINAE, специализацией шаблонов и пр. — это одна из причин сложности.

                                                                      > По-моему работа с сетью никогда не была коньком плюсов. В стандартной либе до сих пор нет ничего для работы с сетью.

                                                                      А без этого всю жизнь обходились. Ибо C++ давал возможность напрямую работать с API конкретной ОС. Плюс абстракции C++ давали возможность строить удобные высокоуровневые средства поверх API ОС. Кроссплатформенные и достаточно эффективные. ACE и Asio тому отличные примеры. Но если эффективности надстроек не хватает, то народ спокойно опускается на работу с API ОС.

                                                                      > Просто я с трудом представляю себе целый проект, который целиком про ковыряние в регистрах, но при этом его пишут на каком-нибудь C#.

                                                                      Под работой с низким уровнем подразумевалась не столько работа с регистрами, сколько работа с системным API, написание компонентов для интеграции в ОС, работу с оборудованием.

                                                                      > Кроссплатформенность — это к java и иже.

                                                                      Как раз нет. C++ один из немногих языков, на котором вы сможете написать ядро приложения для работы и под Windows, и под Linux, и под MacOS, и под iOS, и под Android, и под какой-нибудь Tizen.
                                                                        0
                                                                        То, что в C++ за одной строчкой может скрываться столько внутренней механики, что разработчикам на других языках может даже и не сниться.

                                                                        Как раз в C++ с этим всё гораздо проще. А вот в каком-нибудь питоне вообще непонятно что делается под капотом. Я из-за этого так и не научился внятно его использовать.

                                                                        C++ небезопасен и не прощает ошибок, во всю эту механику приходится плотно погружаться.

                                                                        Вот это поворот! Язык, близкий к железу, требователен к программисту.
                                                                        Могу ошибаться, но на таких языках, как Go, Eiffel, Ada, Java это сделать гораздо сложнее.

                                                                        Вообще не понимаю как можно так говорить о языках со сборщиком мусора.

                                                                        Проблема в том, что глядя на код сложно себе представить что на самом деле здесь происходит (вызов функции или конструктора, количество неявных преобразований)

                                                                        Во-первых: эти проблемы решаемы при правильной методологии. Такие вещи программист осваивает уже через пол года работы.
                                                                        Во-вторых: всё это детский лепет по сравнению с языками с динамической типизацией. Там вообще непонятно что происходит.

                                                                        Между тем, C++ вполне позволяет такое делать.

                                                                        Как я уже писал, микроскопом тоже можно забивать гвозди.

                                                                        Как раз нет. C++ один из немногих языков, на котором вы сможете написать ядро приложения для работы и под Windows, и под Linux, и под MacOS, и под iOS, и под Android, и под какой-нибудь Tizen.

                                                                        Только если это ядро считает сферических коней в вакууме. Как только дело касается чего-то связанного с реальностью, то вся кроссплатформенность плюсов заканчивается. Компилировать каждый раз под новую платформу — это минимум того, через что придётся пройти.
                                                                          0
                                                                          Вообще не понимаю как можно так говорить о языках со сборщиком мусора.

                                                                          Вообще-то в Ada нет сборщика мусора. Но если вас не устраивает тот список, то рассмотрите вот этот: C, C++, Ada, Rust. Пожалуй, только C++ предъявляет столь высокие требования к квалификации программиста для написания библиотек (особенно если в них активно применяется обобщенное программирование).


                                                                          Во-первых: эти проблемы решаемы при правильной методологии. Такие вещи программист осваивает уже через пол года работы.

                                                                          У некоторых 25+ лет работы с C++ за плечами, а озвученные проблемы так никуда и не делись.


                                                                          Как я уже писал, микроскопом тоже можно забивать гвозди.

                                                                          Позволю себе указать на еще одну нашу разработку: RESTinio. Создать там встроенный в приложение HTTP-сервер — дело буквально нескольких строк (см. примеры прямо в README). По вашему — это забивание гвоздей микроскопом?


                                                                          Только если это ядро считает сферических коней в вакууме.

                                                                          Поинтересуйтесь, если будет время и желание, таким проектом, как Dropbox Djinni. Почему он возник, для чего применялся.


                                                                          Компилировать каждый раз под новую платформу — это минимум того, через что придётся пройти.

                                                                          Ну вот тот же SObjectizer, фрагмент кода которого и разбирался в обсуждаемой статье, работает под Win, Lin, macOS, FreeBSD, Android. Мы его как-то ради хохмы под OrangePi собирали. Все работает.

                                                                            0
                                                                            рассмотрите вот этот: C, C++, Ada, Rust.

                                                                            Да, обобщённое программирование на C — очень смешно.
                                                                            Ada, насколько я понял, крайне редко используется.
                                                                            В итоге единственный язык, который в данной категории конкурент C++ — это Rust, и он, внезапно, именно для этого создавался много позже плюсов.

                                                                            По вашему — это забивание гвоздей микроскопом?

                                                                            Ну можно было взять java или C# и написать тот же сервер за 15 минут, или взять готовые либы. А можно взять C++ и получить очень много веселья.

                                                                            Поинтересуйтесь, если будет время и желание, таким проектом, как Dropbox Djinni. Почему он возник, для чего применялся.

                                                                            И как он относится к кроссплатформенности C++?

                                                                            Ну вот тот же SObjectizer, фрагмент кода которого и разбирался в обсуждаемой статье, работает под Win, Lin, macOS, FreeBSD, Android.

                                                                            А что вы там делаете, кроме работы со стандартными либами?
                                                                              0
                                                                              > Да, обобщённое программирование на C — очень смешно.

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

                                                                              > Ada, насколько я понял, крайне редко используется.

                                                                              Ada широко используется в узких и специфических нишах. Но широта применения Ada не имеет отношения к тому, что для разработки библиотек на C++ требуется высокая квалификация.

                                                                              > Ну можно было взять java или C# и написать тот же сервер за 15 минут,

                                                                              Встроить HTTP-сервер на Java/C# в существующее C++ приложение за 15 минут? Ну, может быть, для кого-то это и не проблема.

                                                                              > или взять готовые либы.

                                                                              Мы сделали свою потому, что чужие готовые нас не сильно устраивали.

                                                                              > А можно взять C++ и получить очень много веселья.

                                                                              Ну вот люди берут и без проблем используют.

                                                                              > И как он относится к кроссплатформенности C++?

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

                                                                              > А что вы там делаете, кроме работы со стандартными либами?

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

                                                                                Вы мне приводили в пример языки, которые, как я понял, имеют преимущество перед плюсами в обобщённом программировании. Хотите сказать, что на C удобнее программировать те же контейнеры, чем на плюсах?

                                                                                для разработки библиотек на C++ требуется высокая квалификация

                                                                                Всё никак не могу понять чем C++ отличается в этом плане от других языков. И почему именно библиотек? Для приложений нужна меньшая квалификация?

                                                                                в существующее C++ приложение за 15 минут

                                                                                То, что вы тащите за собой готовый проект, и в нём внезапно понадобился какой-то особенный сервер (раз готовые решения не устраивают), не проблема языка.

                                                                                Ну вот люди берут и без проблем используют.

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

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

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

                                                                                На самом деле это не такая уж большая проблема.

                                                                                Согласен, можно потерпеть. Но до тех пор, пока не используются вещи, зависящие от ОС или железа. Хотя, казалось бы, зачем использовать C++, если тебе не нужен какой-нибудь прямой доступ к памяти.
                                                                                Или вот, помнится, были баги в том же std::thread от msvc под виндой. Не знаю, остались ли сейчас.
                                                                                  0
                                                                                  Вы мне приводили в пример языки, которые, как я понял, имеют преимущество перед плюсами в обобщённом программировании.

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


                                                                                  Всё никак не могу понять чем C++ отличается в этом плане от других языков.

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


                                                                                  Для приложений нужна меньшая квалификация?

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


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

                                                                                  Так, что написать библиотеку на C++ сложно. И будь в языке несколько другие возможности (тот же static if или та же компайл-тайм рефлексия), то библиотеки для C++ писать было бы проще.


                                                                                  А вот когда мощные и удобные библиотеки для C++ есть, то программировать на C++ становится сильно проще. И мы, как разработчики подобных библиотек, видим этому постоянные подтверждения.


                                                                                  Если у Вас не хватает времени написать три слова для пояснения

                                                                                  Пояснение "в трех словах" уже было дано ранее.


                                                                                  Хотя, казалось бы, зачем использовать C++, если тебе не нужен какой-нибудь прямой доступ к памяти.

                                                                                  А это уже совсем другая проблема C++: мало кто сейчас имеет представление о возможностях C++ и о том, что на нем можно делать кроме "прямого доступа к памяти". И это еще одна причина, по которой C++у предпочитают другие языки даже там, где применение C++ выгодно.


                                                                                  PS. Во по этому поводу:


                                                                                  То, что вы тащите за собой готовый проект, и в нём внезапно понадобился какой-то особенный сервер (раз готовые решения не устраивают), не проблема языка.

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

                                                                                    0
                                                                                    Нет. В пример приводились языки, в которых разработка библиотек не требует такой высокой квалификации, как в случае C++. Причем если в библиотеке используется обобщенное программирование, то требования в C++ становятся еще выше.

                                                                                    Ну то есть на C удобнее писать библиотеки?

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

                                                                                    Вы уже десяток написали так-то.

                                                                                    Пояснение «в трех словах» уже было дано ранее.

                                                                                    По поводу Dropbox Djinni не было.

                                                                                    мало кто сейчас имеет представление о возможностях C++ и о том, что на нем можно делать кроме «прямого доступа к памяти».

                                                                                    Мой опыт говорит об обратном: те, кому надо, имеют прекрасное представление, а остальные спокойно пользуются другими инструментами.

                                                                                    Вы, похоже, давно утратили нить разговора. Речь была о том, что на C++ можно писать так, что это будет просто, удобно, лаконично и это не будет вызывать никакой боли.

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

                                                                                    С конкретным примером.

                                                                                    Если Вы о коде со static if, то он принесёт больше боли, чем SFINAE и касты вместе взятые. Меня, как разработчика на C++, успокаивает лишь уверенность в том, что комитет никогда не догадается так «облагораживать» плюсы.
                                                                                      0
                                                                                      > Ну то есть на C удобнее писать библиотеки?

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

                                                                                      > Вы уже десяток написали так-то.

                                                                                      А вы еще и считаете?!!! Ну, если бы начал писать о сложности C++, то и одного бы до сих пор не закончил.

                                                                                      А прекрасную иллюстрацию сложности C++ недавно JetBrains сделал: blog.jetbrains.com/rscpp/cpp-quiz-cpp-russia-2019

                                                                                      > По поводу Dropbox Djinni не было.

                                                                                      Djinni — это всего лишь пример в подтверждение тезиса о том, что на чистом C++ делают ядра кроссплатформенных приложений. Но не хотите разбираться что к чему, не надо.

                                                                                      > Мой опыт говорит об обратном: те, кому надо, имеют прекрасное представление, а остальные спокойно пользуются другими инструментами.

                                                                                      И где же здесь обратное?

                                                                                      > Речь была о том, что Вы ради своего удобства хотите притащить в язык вредные для него конструкции, которые конфликтуют с текущей парадигмой.

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

                                                                                      Поскольку у меня есть (пусть небольшой) опыт работы с D и Ruby, где составом класса можно управлять посредством if-ов и циклов. А так же усть большой опыт работы с C++, в котором это нельзя делать. И поскольку мне приходится писать на C++ непростые (для меня) шаблоны, то почему бы мне не хотеть достигать своих целей меньшими усилиями?

                                                                                      > Меня, как разработчика на C++, успокаивает лишь уверенность в том, что комитет никогда не догадается так «облагораживать» плюсы.

                                                                                      Аминь.
                                                                                        0
                                                                                        Вот не могу понять, как из тезиса «языки, в которых разработка библиотек не требует такой высокой квалификации, как в случае C++» прийти к вопросу о большем удобстве написания библиотек на C. Поделитесь логической цепочкой?

                                                                                        Не требуется квалификации -> проще -> удобнее. Хотя, я не понимаю как Вы сравниваете по этому параметру C и C++. Развлекаться с указателями проще, чем написать шаблонную функцию?

                                                                                        А прекрасную иллюстрацию сложности C++ недавно JetBrains сделал: blog.jetbrains.com/rscpp/cpp-quiz-cpp-russia-2019

                                                                                        Да, несколько сферических примеров в вакууме, которые с 99% вероятностью не встретятся на практике. К тому же большая часть из них просто не скомпилируется.

                                                                                        И где же здесь обратное?

                                                                                        Ваши слова были о том, что «мало кто знает», и поэтому не используют. Мои слова о том, что знают те, кому надо. А те, кому не надо, не использовали бы даже узнав.

                                                                                        в подтверждение тезиса о том, что на чистом C++ делают ядра кроссплатформенных приложений.

                                                                                        Делать могут на чём угодно, но о предпосылках к использованию плюсов в кроссплатформенном коде мне неизвестно.
                                                                                          0
                                                                                          Не требуется квалификации -> проще -> удобнее.

                                                                                          Очевидно, что вы спорите не со мной, а с тараканами в своей голове.


                                                                                          Мои слова о том, что знают те, кому надо. А те, кому не надо, не использовали бы даже узнав.

                                                                                          См.выше, про тараканов, ибо обратного здесь не видно.


                                                                                          К тому же большая часть из них просто не скомпилируется.

                                                                                          Политкорректно это называется "за деревьями леса не вижу".


                                                                                          но о предпосылках к использованию плюсов в кроссплатформенном коде мне неизвестно.

                                                                                          "Вот и выросло поколение" (с)
                                                                                          Между тем вы сами прекрасная иллюстрация того, почему C++ не используют даже там, где от него есть выгода.

                                                                                            0
                                                                                            Очевидно, что вы спорите не со мной, а с тараканами в своей голове.

                                                                                            А с чем конкретно Вы не согласны? Ваше изначальное утверждение о том, что для разработки на языке A, который является расширением (пусть и не строгим) языка Б, нужна большая квалификация, чем для разработки на языке Б, видится мне совершенно бессмысленным ввиду очевидности и отсутствия конструктивных выводов. Я попытался так истрактовать ваше высказывание, чтобы придать ему какое-то значение в контексте разговора. Если был неправ, то каюсь.

                                                                                            ибо обратного здесь не видно

                                                                                            Хм, ваше утверждение: «о языке мало знают, поэтому не используют». Моё утверждение: «язык не используют не потому, что мало о нём знают». Где же тут обратное?

                                                                                            Между тем вы сами прекрасная иллюстрация того, почему C++ не используют даже там, где от него есть выгода.

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

                                                                                              Во-первых, с тем, что речь не только о различиях между C++ и C. В изрядном количестве других языков программирования достаточно базового знания языка для того, чтобы делать библиотеки для этого языка. Т.е. если пишешь прикладной код на C, Ada или Rust, то можешь писать и библиотеки для этих языков. А вот в C++ уровень квалификации (в среднем) для написания прикладного кода и для библиотечного кода нужен разный. Простой пример: прикладной программист может не сильно задумываться о "правиле пяти" или о том, как правильно сделать swap для своего класса (и нужно ли его делать вообще). Тогда как разработчику библиотеки желательно не только иметь обо всем этом представление, но и уметь делать такие вещи чуть ли не на автомате.


                                                                                              Во-вторых, "уровень квалификации", "простота" и "удобство" взаимовляющие друг на друга вещи, но отнюдь не тождественные. Например, на C просто делать библиотеки, но неудобно. На Rust не просто (как и вообще программировать на Rust), но удобно. В C++ и не просто, и неудобно.


                                                                                              Где же тут обратное?

                                                                                              Так это у вас нужно спросить.


                                                                                              Вот что сказал я:


                                                                                              мало кто сейчас имеет представление о возможностях C++ и о том, что на нем можно делать кроме "прямого доступа к памяти".

                                                                                              Вот что на это сказали вы:


                                                                                              Мой опыт говорит об обратном: те, кому надо, имеют прекрасное представление, а остальные спокойно пользуются другими инструментами.

                                                                                              Ваше утверждение отнюдь не обратно моему. Они вообще друг другу не противоречат.


                                                                                              Ну, Вам виднее.

                                                                                              Ну вот как-то так уж получается.


                                                                                              Продолжайте хранить свои тайные знания.

                                                                                              Какие же они тайные? Вам ссылку дали, прошли бы по ней, нашли бы уже интересные ссылки, например, на доклады с CppCon-а. И сами бы эти знания приобрели бы.

                                                                                                0
                                                                                                Простой пример: прикладной программист может не сильно задумываться о «правиле пяти» или о том, как правильно сделать swap для своего класса (и нужно ли его делать вообще). Тогда как разработчику библиотеки желательно не только иметь обо всем этом представление, но и уметь делать такие вещи чуть ли не на автомате.

                                                                                                Согласен. Мне просто кажется, что любой код должен писаться как библиотечный, т. е. для многократного использования.

                                                                                                Какие же они тайные? Вам ссылку дали, прошли бы по ней, нашли бы уже интересные ссылки, например, на доклады с CppCon-а. И сами бы эти знания приобрели бы.

                                                                                                Правильно ли я понимаю, что в доказательство кроссплатформенности C++ Вы приводите в пример Djinni, который создаёт мосты между кодом на плюсах и нативным кодом на платформе? И где тут кроссплатформенность конкретно плюсов?

                                                                                                Например, на C просто делать библиотеки, но неудобно. На Rust не просто (как и вообще программировать на Rust), но удобно. В C++ и не просто, и неудобно.

                                                                                                C предоставляет намного меньше возможностей, чем C++, поэтому и делать проще. Так-то библиотеки на C++ можно писать в чистом C стиле, и разница, насколько мне известно, будет минимальна.
                                                                                                Т. е. C и C++ одинакового порядка сложности языки, просто C++ больше, а значит для новых (относительно C) фич нужно больше знаний.
                                                                                                Поэтому-то я и прицепился к вашему сравнению, потому что относится к делу тут только сравнение с Rust, которое вообще говоря C++ проигрывает. Но в этом нет ничего удивительного, учитывая, что Rust создавался как замена C++.
                                                                                                Касательно Ada не могу сказать, ибо не знаком, но судя по малой распространённости, там своих проблем хватает.
                                                                                                Это я всё к тому, что сравнивая языки, нужно брать их из одной ниши, и в своей нише конкурент у плюсов только раст.

                                                                                                Ваше утверждение отнюдь не обратно моему. Они вообще друг другу не противоречат.

                                                                                                Вы из своего утверждения делали вывод:
                                                                                                И это еще одна причина, по которой C++у предпочитают другие языки даже там, где применение C++ выгодно.

                                                                                                Вот вывод-то у меня и другой: низкая осведомлённость о возможностях C++ не является причиной того, что его используют реже. Точнее, это в какой-то мере так, но влияние минимально, и проблема не исключительно плюсов, а свойственна любой технологии.

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