Light weight C++ Signals/Slots or Event System This assumes there is no need to aggregate results from the listeners of a broadcast signal.

Technically I guess the slots are whatever your std::function holds, so they aren't special. So I guess this is more of a Signals/Connections design.

The "connection" or Signal&lt;T...&gt;::Listener is the owner of the callback. It's ideal to have this owned by the object that has the callback function or on the object your lambda captures. That way when that object goes out of scope so does the callback, which prevents it from being called anymore.

You could dump listeners into a container on the object and forget about them. Or store specifically named instances of them which is very useful for switching the object you're listening to or disconnecting.

template &lt;typename... FuncArgs&gt;
class Signal
{
    using fp = std::function&lt;void(FuncArgs...)&gt;;
    std::forward_list&lt;std::weak_ptr&lt;fp&gt; &gt; registeredListeners;
public:
    using Listener = std::shared_ptr&lt;fp&gt;;

    Listener add(const std::function&lt;void(FuncArgs...)&gt; &amp;cb) {
        // passing by address, until copy is made in the Listener as owner.
        Listener result(std::make_shared&lt;fp&gt;(cb));
        registeredListeners.push_front(result);
        return result;
    }

    void raise(FuncArgs... args) {
        registeredListeners.remove_if([&amp;args...](std::weak_ptr&lt;fp&gt; e) -&gt; bool {
            if (auto f = e.lock()) {
                (*f)(args...);
                return false;
            }
            return true;
        });
    }
};


usage

Signal&lt;int&gt; bloopChanged;

// ...

Signal&lt;int&gt;::Listener bloopResponse = bloopChanged.add([](int i) { ... });
// or
decltype(bloopChanged)::Listener bloopResponse = ...

// let bloopResponse go out of scope.
// or re-assign it
// or reset the shared_ptr to disconnect it
bloopResponse.reset();
Sink arguments (objects we want to make a copy of and store internally) are generally passed by value, then moved into place, so the add function can take its parameter by value rather than const&amp;.
We could perhaps make fp public with a name like Function, and use it for the add function parameter.
raise will make unnecessary copies of the arguments. We can avoid that with perfect forwarding.
We don't need to copy the weak_ptr in the raise lambda, we can use a const&amp;. We also don't need to specify the return value.


.

#include &lt;forward_list&gt;
#include &lt;functional&gt;
#include &lt;memory&gt;

template &lt;class... FuncArgs&gt;
class Signal
{   
public:

    using Function = std::function&lt;void(FuncArgs...)&gt;;
    using Listener = std::shared_ptr&lt;Function&gt;;

    Listener add(Function cb) {
        auto const result = std::make_shared&lt;Function&gt;(std::move(cb));
        listeners.push_front(result);
        return result;
    }

    template&lt;class... Args&gt;
    void raise(Args&amp;&amp;... args) {
        listeners.remove_if([&amp;args...](std::weak_ptr&lt;Function&gt; const&amp; e) {
            if (auto f = e.lock()) {
                (*f)(std::forward&lt;Args&gt;(args)...);
                return false;
            }
            return true;
        });
    }

private:

    std::forward_list&lt;std::weak_ptr&lt;Function&gt;&gt; listeners;
};

#include &lt;iostream&gt;

struct Noisy
{
    Noisy() = default;

    Noisy(Noisy const&amp;) { std::cout &lt;&lt; "copy\n"; }
    Noisy&amp; operator=(Noisy const&amp;) { std::cout &lt;&lt; "copy assign\n"; return *this; }

    Noisy(Noisy&amp;&amp;) { std::cout &lt;&lt; "move\n"; }
    Noisy&amp; operator=(Noisy&amp;&amp;) { std::cout &lt;&lt; "move assign\n"; return *this; }
};

int main()
{
    {
        auto s = Signal&lt;int&gt;();
        auto const l = s.add([] (int v) { std::cout &lt;&lt; "value: " &lt;&lt; v &lt;&lt; std::endl; });
        s.raise(5);
    }
    {
        auto s = Signal&lt;Noisy const&amp;&gt;();
        auto const l = s.add([] (Noisy const&amp;) { });
        auto const n = Noisy();
        s.raise(n);
    }
}