Reputation: 606
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:
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.
Use std::any
instead of Message
inside MessagePump
.
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.
Use C++20 concepts? (I would like also to know how to solve it with C++17).
Mix Ad.4 and Ad.5: use template parameter but explicitly define what interface it shall implement.
Other?
Upvotes: 1
Views: 235
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
Reputation: 418
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.
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.
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.
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.
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.
use template parameter but explicitly define what interface it shall implement. Basically what concepts are meant to achieve.
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