Интерфейс с функциями, которые принимают любой тип - PullRequest
1 голос
/ 05 мая 2020

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

В настоящее время я сделал это вот так:

template <typename Message>
struct message_pump
{
    virtual void send(Message &&) = 0;

    //! Blocks if no message is available.
    virtual Message receive() = 0;
};

Тогда я хотел бы использовать этот message_pump интерфейс как член класса active (шаблон из Herb Sutters '- «Предпочитать использование активных объектов вместо голых потоков» ):

template <typename Message>
class active
{
  private:
    struct quit_message{};
    using MessageProxy = typename std::variant<Message, quit_message>;

    std::unique_ptr<message_pump<MessageProxy>> message_pump_impl;
    std::function<void(Message&&)> message_handler;
    std::thread worker_thread;

    void thread_code() {
        while(true)
        {
            auto m{message_pump_impl->receive()};
            if(std::holds_alternative<quit_message>(m))
                break;
            message_handler(std::move(std::get<Message>(m)));
        }
    }

  public:
    active(std::unique_ptr<message_pump<MessageProxy>> message_pump_impl,
           std::function<void(Message&&)> message_handler) : 
               message_pump_impl{std::move(message_pump_impl)},
               message_handler{message_handler},
               worker_thread{[this](){ this->thread_code(); }} {}

};

Проблема здесь в том, что полиморфизм stati c и Dynami c плохо сочетается, и невозможно внедрить реализацию message_pump, не зная тип базовый Message.

Причина, по которой я делаю этот класс active, заключается в том, что я хотел бы повторно использовать его в различных ОСРВ, которые предоставляют разные реализации классов queue и thread, и при этом иметь возможность тестировать его. на локальном компьютере. (Я поместил в этот листинг std::thread только для упрощения, потому что как сделать реализацию thread инъектируемой - это другой topi c).

Вопрос в том, какой способ предпочтительнее - самый OOP и способ «как это должно быть сделано» - определить интерфейс message_pump, чтобы иметь возможность легко внедрить реализацию в класс active?

У меня есть несколько решений:

  1. Определите struct message {} внутри message_pump и сделайте MessageProxy структурой, наследуемой от message_pump::message. Затем верните std::unique_ptr<message> из функции интерфейса receive().

  2. Используйте std::any вместо Message внутри MessagePump.

  3. Использовать статический c полиморфизм и внедрять message_pump реализацию через параметр шаблона. Тогда интерфейс message_pump не нужно определять явно, и мы получим ошибки компилятора, если разработчик не имеет специальной функции c.

  4. Используйте C ++ 20 концепции? (Я также хотел бы знать, как решить эту проблему с помощью C ++ 17).

  5. Смешайте Ad.4 и Ad.5: используйте параметр шаблона, но явно укажите, какой интерфейс он должен реализовать .

  6. Другое?

Ответы [ 2 ]

1 голос
/ 05 мая 2020
  1. Использовать динамический c полиморфизм. Это работает, однако для этого требуется тип active, чтобы раскрыть, какой тип он использует внутри для хранения сообщений, чтобы можно было построить правильный тип очереди.

  2. Define struct message {} внутри message_pump и сделайте MessageProxy структурой, наследуемой от message_pump::message. Это ничего нам не даст, если мы используем полиморфизм динамического c. Однако при использовании полиморфизма stati c пользовательские сообщения могут быть производными от active::message, который может производиться от message_pump::message, что позволяет добавлять неподвижные сообщения без методов "emplace". Необходимость извлекать сообщения из active::message - это недостаток, который нельзя упускать из виду, особенно если в противном случае сообщение может повторно использовать существующий тип, такой как int. Это также потребует динамического распределения сообщений даже для очередей, которые обычно этого не требуют.

  3. Используйте std::any вместо Message внутри MessagePump. При использовании динамического c полиморфизма, он решает проблему с active, который должен раскрывать, какой тип сообщений он использует внутри, и позволяет active не быть шаблоном. Но потери stati c проверки типов и накладные расходы времени выполнения. Я бы не рекомендовал это, поскольку проверка типов stati c может сделать рефакторинг намного менее подверженным ошибкам.

  4. Используйте полиморфизм stati c и введите реализацию message_pump с помощью параметра шаблона . Если message_pump является параметром шаблона шаблона, active не нужно будет раскрывать, какой тип сообщений он использует внутри. Это похоже на подход, используемый стандартной библиотекой. Однако сообщения об ошибках могут быть трудными для понимания.

  5. Использовать концепции C ++ 20? (Хотелось бы также узнать, как это решить с помощью C ++ 17). концепции могут помочь документировать, какие методы понадобятся message_pump, и могут дать более точные ошибки. Я бы не стал пробовать что-либо подобное с c ++ 17, поскольку версии c ++ 17, как правило, трудно читать и в этом случае дают мало пользы.

  6. использовать параметр шаблона но явно определите, какой интерфейс он должен реализовать. В основном, какие концепции предназначены для реализации.

  7. Другое? реализовать очередь, которая работает на нескольких платформах, возможно, используя #ifdef, а active использует эту очередь или параметр шаблона active по умолчанию message_pump для этой очереди.

0 голосов
/ 05 мая 2020

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

struct Message { std::string payload{ "Hello" }; };

struct VTable
{
    void* ( *Receive )( void* ptr );
    void  ( *Send )( void* ptr, void* message );
    void  ( *Destroy_ )( void* ptr );
    void* ( *Clone_ )( void* ptr );
    void* ( *MoveClone_ )( void* ptr );
};

template<typename T>
constexpr VTable VTableFor
{
    [ ]( void* ptr ) -> void* { return static_cast<T*>( ptr )->Receive( ); },
    [ ]( void* ptr, void* message ) { static_cast<T*>( ptr )->Send( message ); },
    [ ]( void* ptr ) { delete static_cast<T*>( ptr ); },
    [ ]( void* ptr ) -> void* { return new T{ *static_cast<T*>( ptr ) }; },
    [ ]( void* ptr ) -> void* { return new T{ std::move( *static_cast<T*>( ptr ) ) }; }
};


struct MessagePump 
{
    void* concrete_;
    const VTable* vtable_;

    template<typename T>
    MessagePump( T&& t ) 
        : concrete_( new T{ std::forward<T>( t ) } ),
          vtable_{ &VTableFor<T> } { }

    MessagePump( const MessagePump& rhs ) noexcept
        : concrete_{ rhs.vtable_->Clone_( rhs.concrete_ ) },
          vtable_{ rhs.vtable_ } { }

    MessagePump( MessagePump&& rhs ) noexcept
        : concrete_{ rhs.vtable_->MoveClone_( rhs.concrete_ ) },
          vtable_{ rhs.vtable_ } { }

    MessagePump& operator=( MessagePump rhs ) noexcept
    {
        swap( *this, rhs );      
        return *this;
    }

    friend void swap( MessagePump& lhs, MessagePump& rhs ) noexcept
    {
        using std::swap;
        swap( lhs.concrete_, rhs.concrete_ );
        swap( lhs.vtable_, rhs.vtable_ );
    }

    void* Receive( ) { return vtable_->Receive( concrete_ ); }

    void Send( void* message ) { vtable_->Send( concrete_, message ); }

    ~MessagePump( ) { vtable_->Destroy_( concrete_ ); }
};

struct CustomPump
{
    void* Receive( ) 
    { return new Message{ }; };

    void Send( void* message )
    {
        auto ptr{ static_cast<Message*>( message ) };
        std::cout << "Sending: " << ptr->payload << '\n';
        delete ptr;
    }
};

template<typename MessageType>
class Active
{
public:
    using Callback = void( * )( MessageType* msg );

    Active( MessagePump pump, Callback cb )
        : pump_{ std::move( pump ) },
          cb_{ cb } { }

    void Start( )
    {
        while ( true )
        {
            auto message{ pump_.Receive( ) };
            if ( !message )
            {
                std::cout << "No message\n";
                break;
            }
            else
            {
                auto message{ static_cast<MessageType*>( result ) };
                std::invoke( cb_, message );
                pump_.Send( message );
            } 
        }
    }
private:
    MessagePump pump_;
    Callback cb_;
};


int main ( )
{
    Active<Message> active{ CustomPump{ }, 
        [ ]( Message* msg ){ std::cout << "Received: " << msg->payload << '\n'; } };
    active.Start( );
}
...