Rocket C++ Signal/Slots Library

repository·master·Indexed 42 days ago

https://github.com/tripleslash/rocket

A high-performance, single-header C++ signal/slots library for the Observer pattern. Designed as a lightweight, dependency-free alternative to boost::signals2, it features rocket::signal for single-threaded use and rocket::thread_safe_signal for thread-safe operations. Key capabilities include automatic lifetime tracking via rocket::trackable, scoped_connection for RAII management, queued_connection for cross-thread message queuing, and support for value collectors and signal emission abortion.

Tokens
3K
Snippets
8
Records
9
Agent score
3%

What's inside Rocket

  1. Introduction to Rocket

    master

    Rocket is a high-performance, single-header C++ signal/slots library (Observer Pattern). It is designed to be a lightweight, dependency-free alternative to boost::signals2.

    Key features include:

    • Efficiency: Minimal overhead, avoids cache-unfriendly virtual calls where possible, and does not allocate during signal emission.
    • Thread Safety Options: Provides both rocket::signal (thread-unsafe, optimized for single-threaded use) and rocket::thread_safe_signal (thread-safe).
    • Reentrancy: Signals can be recursively emitted, and slots can disconnect themselves during emission.
    • Modern C++: Single-header implementation with no external dependencies.
    • Advanced Features: Support for scoped_connection, automatic lifetime tracking via rocket::trackable, Qt-style queued_connection, and signal emission abortion.
  2. Manage connection lifetimes with scoped_connection and trackable

    master

    Rocket provides two primary ways to prevent dangling pointers when an observer is destroyed:

    1. rocket::scoped_connection: A RAII object that automatically disconnects the slot when it goes out of scope. Use this if you want to manage the connection lifetime manually or within a specific block.
    2. rocket::trackable: If an observer class inherits from rocket::trackable, the signal will automatically detect when the object is destroyed and disconnect any associated slots. This is the most automated way to handle lifetime tracking.

    Note: For trackable to work, the class must inherit from rocket::trackable.

    // Using scoped_connection
    {
        std::unique_ptr<ILogger> logger = std::make_unique<ConsoleLogger>();
        rocket::scoped_connection connection = app->onSuccess
            .connect(logger.get(), &ILogger::logMessage);
        app->run();
    }
    // logger is destroyed, connection is destroyed, slot is disconnected.
    
    // Using rocket::trackable
    struct ILogger : rocket::trackable {
        virtual void logMessage(const std::string& message) = 0;
    };
    // When ILogger instance is destroyed, the signal automatically disconnects.
  3. Connect class methods to a signal

    master

    You can connect member functions of a class instance to a signal. This is typically done by passing the instance pointer to the connect method.

    To ensure connections are cleaned up when the observer is destroyed, store the returned connection object in a rocket::scoped_connection_container or a rocket::scoped_connection.

    class Subject {
    public:
        void setName(const std::string& newName) {
            if (name != newName) {
                name = newName;
                nameChanged(newName);
            }
        }
        rocket::signal<void(std::string)> nameChanged;
    private:
        std::string name;
    };
    
    class Observer {
    public:
        Observer(Subject& subject) {
            // Store connection in a container to break it when Observer is destroyed
            connections += {
                subject.nameChanged.connect<&Observer::onNameChanged>(this)
            };
        }
    
        void onNameChanged(const std::string& name) {
            std::cout << "Subject received new name: " << name << std::endl;
        }
    
    private:
        rocket::scoped_connection_container connections;
    };
  4. Create and invoke a signal

    master

    To use Rocket, define a signal with a specific signature using rocket::signal<Signature> or rocket::thread_safe_signal<Signature>. You can connect handlers using the .connect() method or the += operator. Invoking the signal is done by calling the signal object as a function signal().

    #include <iostream>
    #include <rocket.hpp> // Assuming header name
    
    int main()
    {
        rocket::signal<void()> thread_unsafe_signal;
        rocket::thread_safe_signal<void()> thread_safe_signal;
    
        // Connecting the first handler to our signal
        thread_unsafe_signal.connect([]() {
            std::cout << "First handler called!" << std::endl;
        });
        
        // Connecting a second handler to our signal using alternative syntax
        thread_unsafe_signal += []() {
            std::cout << "Second handler called!" << std::endl;
        };
        
        // Invoking the signal
        thread_unsafe_signal();
    }
  5. Schedule timers with `set_interval` and `set_timeout`

    master

    Rocket provides timer scheduling capabilities that behave similarly to queued_connection. Timers are scheduled to execute their associated slots within the thread that called the timer setup function.

    To make these timers work, you must call rocket::dispatch_queued_calls() in your application loop. It is recommended to store the returned connection objects in a rocket::scoped_connection_container to manage their lifetime automatically.

    // Inside an initialization method
    connections += {
        // Register a timer that gets called every 5000 ms
        rocket::set_interval<&RenderThread::onClearRenderablesTimerExpired>(this, 5000),
        
        // Other connections...
    };
    
    // In the main loop
    void RenderThread::render() {
        rocket::dispatch_queued_calls(); // Required to trigger the timer slots
        // ...
    }
  6. Build a thread-safe message queue with `queued_connection`

    master

    You can use the rocket::queued_connection flag when calling connect to create a message queue between threads. Instead of executing the slot immediately in the thread that invokes the signal, rocket schedules the execution to occur in the thread where the connect call was originally made.

    To process these queued calls, you must periodically call rocket::dispatch_queued_calls() in the target thread (e.g., your main render loop). This ensures that the slots are executed within the context of that thread, allowing you to modify thread-local data without explicit locking.

    // 1. Connect using the queued_connection flag
    modelLoaderThread.modelLoaded.connect<&RenderThread::onModelLoaded>(this, rocket::queued_connection);
    
    // 2. In your target thread's loop, dispatch the calls
    void RenderThread::render() {
        rocket::dispatch_queued_calls(); // Executes the queued slots on this thread
        // ... rest of render logic
    }
    
    // 3. The slot is now thread-safe relative to the RenderThread's data
    void RenderThread::onModelLoaded(ModelFilePtr const& modelFile) {
        renderables.push_back(new ModelRenderer(modelFile));
    }
  7. Handle return values from signals using value collectors

    master

    When a signal's signature includes a return type (e.g., rocket::signal<int(int)>), the signal can collect values from its connected slots.

    • Default Behavior: The signal returns an std::optional<T> containing the result of the slots.
    • Custom Collectors: You can specify a value collector at declaration time (e.g., rocket::range<T>) to change how results are aggregated.
    • Invocation-time Overrides: You can use .invoke<Collector>(args...) to specify a different collection policy for a single call.

    Common collectors include rocket::range<T>, rocket::first<T>, and rocket::last<T>.

    // Default: returns optional<int>
    rocket::signal<int(int)> signal;
    signal.connect(std::sqrtf);
    int result = *signal(16);
    
    // Using rocket::range to get all results
    rocket::signal<float(float), rocket::range<float>> signal;
    signal.connect(std::sinf);
    signal.connect(std::cosf);
    for (auto result : signal(3.14159)) { /* ... */ }
    
    // Overriding at invocation time
    float first = signal.invoke<rocket::first<float>>(3.14159);
    float last = signal.invoke<rocket::last<float>>(3.14159);
  8. Access the current connection inside a slot

    master

    A slot can access its own connection object using rocket::current_connection(). This is useful for implementing 'one-shot' callbacks that disconnect themselves immediately after being triggered.

    rocket::signal<void()> signal;
    
    signal.connect([] {
        std::cout << "Slot called. Now disconnecting..." << std::endl;
        // Disconnects this specific connection from the signal
        rocket::current_connection().disconnect();
    });
    
    signal(); // Fires
    signal(); // Does nothing
  9. Preemptively abort signal emission

    master

    A slot can stop the execution of subsequent slots in the same emission cycle by calling rocket::abort_emission(). This does not disconnect the slots; it simply breaks the loop of the current signal invocation. This is useful for 'first-match' logic where you only care about the first slot that produces a result.

    rocket::signal<void()> signal;
    
    signal.connect([] {
        std::cout << "First slot called. Aborting emission of other slots." << std::endl;
        rocket::abort_emission();
    });
     
    signal.connect([] {
        std::cout << "Second slot called. Should never happen." << std::endl;
    });
    
    signal();