6
\$\begingroup\$

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<T...>::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 <typename... FuncArgs>
class Signal
{
    using fp = std::function<void(FuncArgs...)>;
    std::forward_list<std::weak_ptr<fp> > registeredListeners;
public:
    using Listener = std::shared_ptr<fp>;

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

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

usage

Signal<int> bloopChanged;

// ...

Signal<int>::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();
\$\endgroup\$
1
  • \$\begingroup\$ One design question I had was if it was worth wrapping the "std::shared_ptr<fp>" in a totally different object that has a more canonical interface. Though pretty much the only thing it would do is rename reset to disconnect, and reset seems fine, so this kept the implementatiton really minimal. \$\endgroup\$
    – johnb003
    Oct 12, 2019 at 2:17

1 Answer 1

4
\$\begingroup\$
  • 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&.

  • 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&. We also don't need to specify the return value.

.

#include <forward_list>
#include <functional>
#include <memory>

template <class... FuncArgs>
class Signal
{   
public:

    using Function = std::function<void(FuncArgs...)>;
    using Listener = std::shared_ptr<Function>;

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

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

private:

    std::forward_list<std::weak_ptr<Function>> listeners;
};

#include <iostream>

struct Noisy
{
    Noisy() = default;

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

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

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

\$\endgroup\$
3
  • \$\begingroup\$ Nice feedback. I tried analyzing the std::function target to see if I could determine the address and realized even if passing by reference, some kind of copy was made automatically anyway. Good catch on the forwarding. As for the using for the Function, I was a little torn on whether or not to alias it. As part of the public interface it seemed appropriate to be more direct. But certainly it makes the implementation a little cleaner to alias it. \$\endgroup\$
    – johnb003
    Oct 13, 2019 at 6:24
  • \$\begingroup\$ I'm not sure it's a good idea to force the args to be an rvalue though. What if someone wants to pass an argument that's already an lvalue? \$\endgroup\$
    – johnb003
    Oct 13, 2019 at 6:34
  • 1
    \$\begingroup\$ In the altered version, raise was changed to be a template function, and the args are now template arguments of the template function. The type deduction means that the Args&& are "universal" or "forwarding" references, rather than r-value references. More details: eli.thegreenplace.net/2014/… \$\endgroup\$
    – user673679
    Oct 13, 2019 at 11:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.