K. Koovalsky
K. Koovalsky

Reputation: 606

Interface with functions that take any type

I would like to define an interface for a message pump that is able to send and receive messages with a type specified by the user, to communicate e.g. between a producer and consumer.

Currently I have done it like that:

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

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

Then I would like to use this message_pump interface as a member of an active class (the pattern from Herb Sutters' - "Prefer Using Active Objects Instead of Naked Threads"):

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(); }} {}

};

The problem here is that static and dynamic polymorphism doesn't mix well and it's not possible to inject the implementation of the message_pump without knowing the type of the underlying Message.

The reason I'm doing this active class is that that I would like to reuse it across various RTOSes, which provide different queue and thread class implementations, and still be able to test it on the local computer. (I've put in this listing std::thread just for simplification, because how to make the thread implementation injectable is a different topic).

The question is what's the preferred way - the most OOP and "as it should be done" way - to define the interface message_pump, to be able to easily inject the implementation to the active class?

I have a few solutions in mind:

  1. Define struct message {} inside message_pump and make MessageProxy a struct which inherits from the message_pump::message. Then return std::unique_ptr<message> from the receive() interface function.

  2. Use std::any instead of Message inside MessagePump.

  3. Use static polymorphism and inject message_pump implementation through a template parameter. Then the message_pump interface is not needed to be defined explicitly and we will get compiler errors if the implementer doesn't have a specific function.

  4. Use C++20 concepts? (I would like also to know how to solve it with C++17).

  5. Mix Ad.4 and Ad.5: use template parameter but explicitly define what interface it shall implement.

  6. Other?

Upvotes: 1

Views: 235

Answers (2)

WBuck
WBuck

Reputation: 5503

OK, you can use type erasure and opaque pointers in order to hide the details about both the message pump and message.

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( );
}

Upvotes: 0

Isaac Clancy
Isaac Clancy

Reputation: 418

  1. Use dynamic polymorphism. This works, however, it requires the active type to expose what type it uses internally to hold the messages so that the right type of queue can be constructed.

  2. Define struct message {} inside message_pump and make MessageProxy a struct which inherits from the message_pump::message. This doesn't gain us anything if we are using dynamic polymorphism. However, if using static polymorphism user messages can derive from active::message which can derive from message_pump::message, which makes it possible to add non-movable messages without "emplace" methods. Having to derive messages from active::message is a down-side that shouldn't be overlooked especially if a message could reuse an existing type such as int otherwise. This will also require the messages to be dynamically allocated even for queues that wouldn't normally require this.

  3. Use std::any instead of Message inside MessagePump. If using dynamic polymorphism, it fixes the problem with active having to expose what type of messages it uses internally and allows active to not be a template. But losses static type checking and has runtime overhead. I wouldn't recommend this as static type checking can make refactoring a lot less error prone.

  4. Use static polymorphism and inject message_pump implementation through a template parameter. If message_pump is a template template parameter, active won't have to expose what type of messages it uses internally. This is similar to the approach taken by the standard library. However, the error messages can be hard to understand.

  5. Use C++20 concepts? (I would like also to know how to solve it with C++17). concepts can help document what methods a message_pump would need and might give better errors. I wouldn't try anything like this with c++17, as the c++17 versions tend to be hard to read and give little benefit in this case.

  6. use template parameter but explicitly define what interface it shall implement. Basically what concepts are meant to achieve.

  7. Other? implement a queue that works on multiple platforms perhaps using #ifdef and have active use that queue or default active's message_pump template parameter to that queue.

Upvotes: 1

Related Questions