Implementing a Lightweight Inter-Module Messaging System in C++ with Sigslot
Inter-module communication in C++ applications often requires a flexible, type-safe, and lightweight event distribution system. While frameworks like Qt or Boost provide robust signal-slot implementations, a single-header library like sigslot offers a minimal footprint without external dependencies. The following architecture demonstrates how to construct a publish-subscribe messaging layer where a central dispatcher broadcasts structured payloads to registered handlers.
Event Payload Structure To standardize cross-component communication, a unified data container is defined. This container carries a unique identifier for routing and a generic pointer to accommodate arbitrary data types without template bloat.
struct BroadcastMessage {
std::string category;
void* payload;
};
By encapsulating the data in a void* pointer, the system avoids signature mismatches across different event types. Consumers are responsible for casting the pointer back to the expected type based on the category field.
The Dispatcher Component
The dispatcher acts as the origin point for events. It manages the binding lifecycle of subscribers and triggers notifications. Internally, it wraps a sigslot::signal1 templated to accept a pointer to the message structure.
class EventDispatcher {
public:
void triggerEvent(const std::string& evtCategory, void* data);
void registerHandler(EventListener* handler);
void unregisterHandler(EventListener* handler);
private:
sigslot::signal1<BroadcastMessage*> m_dispatchSignal;
};
The core operations are straightforward:
triggerEvent: Constructs a temporary message instance, populates it, and invokes the signal. The stack-allocated object remains valid until all connected handlers complete their execution.registerHandler/unregisterHandler: Leveragessigslot'sconnectanddisconnectmethods to manage subscriber pointers.
void EventDispatcher::triggerEvent(const std::string& evtCategory, void* data) {
BroadcastMessage evt;
evt.category = evtCategory;
evt.payload = data;
m_dispatchSignal(&evt);
}
void EventDispatcher::registerHandler(EventListener* handler) {
m_dispatchSignal.connect(handler, &EventListener::processEvent);
}
void EventDispatcher::unregisterHandler(EventListener* handler) {
m_dispatchSignal.disconnect(handler);
}
The Subscriber Interface
sigslot requires handlers to inherit from a specific base class to enable automatic disconnection on destruction. We define a concrete interface for event consumers:
class EventListener : public sigslot::has_slots<> {
public:
virtual void processEvent(BroadcastMessage* msg) = 0;
};
Concrete implementations override processEvent to filter messages by category and extract the payload. For instance, a UI update module might listen for specific state changes:
class UiUpdateModule : public EventListener {
public:
explicit UiUpdateModule(EventDispatcher& dispatcher) {
dispatcher.registerHandler(this);
}
void processEvent(BroadcastMessage* msg) override {
if (msg->category == "UI_REFRESH") {
std::cout << "UI Module: Refreshing interface.\n";
}
}
};
Validation and Usage To verify the architecture, two distinct handler are instantiated against a shared dispatcher. Each handler filters for a specific event category and processes the attached payload accordingly.
class NetworkHandler : public EventListener {
public:
explicit NetworkHandler(EventDispatcher& dispatcher) {
dispatcher.registerHandler(this);
}
void processEvent(BroadcastMessage* msg) override {
if (msg->category == "NET_PACKET") {
const char* info = static_cast<const char*>(msg->payload);
std::cout << "Network: Processing " << info << "\n";
}
}
};
class LogHandler : public EventListener {
public:
explicit LogHandler(EventDispatcher& dispatcher) {
dispatcher.registerHandler(this);
}
void processEvent(BroadcastMessage* msg) override {
if (msg->category == "SYS_LOG") {
const char* info = static_cast<const char*>(msg->payload);
std::cout << "Logger: Recording " << info << "\n";
}
}
};
Integration in an appplication entry point follows a simple sequence:
int main() {
EventDispatcher dispatcher;
NetworkHandler netListener(dispatcher);
LogHandler logListener(dispatcher);
dispatcher.triggerEvent("NET_PACKET", reinterpret_cast<void*>(const_cast<char*>("TCP_ACK")));
dispatcher.triggerEvent("SYS_LOG", reinterpret_cast<void*>(const_cast<char*>("Connection established")));
return 0;
}
Execution produces output strictly aligned with the registered filters:
Network: Processing TCP_ACKLogger: Recording Connection established
This pattern isolates event producers from consumers, enabling modular plugin architectures. By relying on stack-allocated message objects and standard pointer bindings, the system maintains low overhead while supporting runtime dynamic registration and unregistration.