Base class for all Task Framework components
The SignalSlot
class implements a thread-safe event notification system that enables loosely
coupled communication between components. It provides a mechanism for objects to register interest in
events (signals) and receive notifications when those events occur, without requiring direct
dependencies between the signal emitter and receiver.
This implementation supports both parameterless signals and signals with arbitrary arguments, providing a powerful foundation for event-driven programming patterns in concurrent environments.
ArgumentPack
The SignalSlot implementation is fully thread-safe, with the following guarantees:
class SignalSlot {
public:
// Constructor & destructor
SignalSlot(std::ostream &output = std::cerr);
virtual ~SignalSlot();
// Output stream control
void setOutputStream(std::ostream &stream);
std::ostream &stream() const;
// Signal creation
bool createSignal(const std::string &name);
// Signal checking
bool hasSignal(const std::string &name) const;
// Unified connection method (auto-detects argument requirements)
template
ConnectionHandle connect(const std::string &name, Func &&slot);
// Connect member function (auto-detects argument requirements)
template
ConnectionHandle connect(const std::string &name, T *instance, Method method);
// Signal emission
void emit(const std::string &name, SyncPolicy policy = SyncPolicy::Direct);
void emit(const std::string &name, const ArgumentPack &args,
SyncPolicy policy = SyncPolicy::Direct);
void emitString(const std::string &name, const std::string &value,
SyncPolicy policy = SyncPolicy::Direct);
// Disconnect all signals
void disconnectAllSignals();
private:
// Internal signal access methods
std::shared_ptr getSignal(const std::string &name) const;
// Thread-safe storage
mutable std::mutex m_signalsMutex;
std::map> m_signals;
mutable std::mutex m_streamMutex;
std::reference_wrapper m_output_stream;
};
The updated implementation uses a unified Signal class that can handle both parameterless callbacks and callbacks that take arguments:
Handler Type | Description | Use Case |
---|---|---|
void() |
Simple notification without arguments | Status changes, triggers, completion events |
void(const ArgumentPack&) |
Handler with argument data | Data updates, errors with details, results |
The SignalSlot system supports different synchronization policies for signal emission:
Policy | Description | Use Case |
---|---|---|
SyncPolicy::Direct |
Execute handlers directly in the emitting thread | Default behavior, synchronous execution |
SyncPolicy::Blocking |
Block until all handlers have completed | When confirmed completion is required |
// Class using the SignalSlot system
class FileDownloader : public SignalSlot {
public:
FileDownloader() {
// Create signals for various events
createSignal("downloadStarted");
createSignal("downloadCompleted");
createSignal("progress");
createSignal("error");
}
void startDownload(const std::string& url) {
// Signal that download is starting
emit("downloadStarted");
try {
// Simulate download process
for (int i = 0; i <= 10; ++i) {
// Report progress
ArgumentPack progressArgs;
progressArgs.add(i / 10.0f);
progressArgs.add(url);
emit("progress", progressArgs);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Signal completion
emit("downloadCompleted");
}
catch (const std::exception& e) {
// Signal error
emitString("error", std::string("Download failed: ") + e.what());
}
}
};
// Using the SignalSlot system
void downloadExample() {
// Create the downloader
auto downloader = std::make_shared();
// Connect to signals with lambda functions - no arguments
downloader->connect("downloadStarted", []() {
std::cout << "Download has started!" << std::endl;
});
downloader->connect("downloadCompleted", []() {
std::cout << "Download completed successfully!" << std::endl;
});
// Connect to signals with lambda functions - with arguments
downloader->connect("progress", [](const ArgumentPack& args) {
float progress = args.get(0);
std::string url = args.get(1);
std::cout << "Downloading " << url << ": "
<< (progress * 100) << "%" << std::endl;
});
downloader->connect("error", [](const ArgumentPack& args) {
std::string errorMsg = args.get(0);
std::cerr << "ERROR: " << errorMsg << std::endl;
});
// Start the download
downloader->startDownload("https://example.com/file.zip");
}
// Multi-threaded usage example
void threadedExample() {
auto signalSource = std::make_shared();
signalSource->createSignal("event");
// Connect handlers in the main thread
signalSource->connect("event", [](const ArgumentPack& args) {
int value = args.get(0);
std::cout << "Event received with value: " << value << std::endl;
});
// Create multiple worker threads that emit signals
std::vector threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([signalSource, i]() {
for (int j = 0; j < 10; ++j) {
// Create arguments
ArgumentPack args;
args.add(i * 100 + j);
args.add("Thread " + std::to_string(i));
// Emit signal safely from this thread
signalSource->emit("event", args);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
});
}
// Wait for all threads to complete
for (auto& thread : threads) {
thread.join();
}
}
The ArgumentPack
class provides a type-safe container for passing arbitrary arguments with
signals:
// Creating an ArgumentPack
ArgumentPack args;
args.add("filename.txt");
args.add(42);
args.add(3.14159);
// Direct creation with variadic constructor
ArgumentPack args2("filename.txt", 42, 3.14159);
// Retrieving values by index and type
std::string filename = args.get(0);
int count = args.get(1);
double value = args.get(2);
// Getting the number of arguments
size_t numArgs = args.size();
// Checking if empty
bool isEmpty = args.empty();
// Cloning an ArgumentPack (deep copy)
ArgumentPack copy = args.clone();
get<T>(index)
method will throw a
std::bad_cast
exception if the requested type doesn't match the stored type.
The SignalSlot system provides a ConnectionHandle
for managing signal-slot connections:
// Get a connection handle when connecting to a signal
ConnectionHandle connection = object->connect("dataReceived", myCallback);
// Later, disconnect when no longer needed
connection->disconnect();
// Check if still connected
if (connection->connected()) {
// ...
}
Connection handles use shared ownership to ensure connections are properly managed even when handlers go out of scope or are accessed from multiple threads.
The SignalSlot system allows signals to be safely emitted across thread boundaries:
// Thread 1: Set up signals and connections
auto manager = std::make_shared();
manager->createSignal("taskCompleted");
// Thread 2: Emit signals
std::thread worker([manager]() {
// Perform work...
// Create result arguments
ArgumentPack results;
results.add(42);
results.add("Task completed successfully");
// Safely emit signal from worker thread
manager->emit("taskCompleted", results);
});
ConnectionHandles ensure connections are properly managed across threads:
std::vector connections;
std::mutex connectionsMutex;
// Thread 1: Create connections
{
std::lock_guard lock(connectionsMutex);
connections.push_back(source->connect("event", handler1));
connections.push_back(source->connect("event", handler2));
}
// Thread 2: Disconnect a specific connection
{
std::lock_guard lock(connectionsMutex);
if (!connections.empty()) {
connections[0]->disconnect();
}
}
The SignalSlot class provides a robust, thread-safe foundation for event-based communication in the Task Framework. By enabling loosely coupled interaction between components, even across thread boundaries, it promotes modular, maintainable code structures that can be easily extended and modified in concurrent applications.
This implementation balances flexibility, type safety, and thread safety, making it suitable for a wide range of concurrent application requirements. The consistent interface across the framework ensures that all components can participate in the signal-slot communication system with minimal integration effort.