Skip to content

Instantly share code, notes, and snippets.

@johnb003
Last active October 14, 2019 05:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johnb003/dbc4a69af8ea8f4771666ce2e383047d to your computer and use it in GitHub Desktop.
Save johnb003/dbc4a69af8ea8f4771666ce2e383047d to your computer and use it in GitHub Desktop.
Very lightweight Signals / Slots mechanism for C++, with assumed return void. (No need to aggregate).
#include <forward_list>
#include <functional>
#include <memory>
template <typename... FuncArgs>
class Signal
{
using Function = std::function<void(FuncArgs...)>;
std::forward_list<std::weak_ptr<Function> > registeredListeners;
public:
using Listener = std::shared_ptr<Function>;
Listener add(Function cb) {
// cb was passed by address. When creating the shared ptr, we copy it by value
// cb is often an r value lambda so, we need to make sure we copy it.
auto const result = std::make_shared<Function>(std::move(cb));
registeredListeners.push_front(result);
return result;
}
// Don't be fooled by FuncArgs&& (It's not a r-value reference)
// The redundant template specification actually changes this for perfect forwarding.
// See: https://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c
template <typename... FuncArgs>
void raise(FuncArgs&&... args) {
registeredListeners.remove_if([&args...](const std::weak_ptr<Function> &e) {
if (auto f = e.lock()) {
(*f)(std::forward<FuncArgs>(args)...);
return false;
}
return true;
});
}
};
// To Use:
// Store some signal provider
// Signal<int> eventThing;
// ...
// Register a callback for the signal, and store the listener owning reference:
// Signal<int>::Listener listener = eventThing.add([](int) { ... });
// when `listener` goes out of scope, the callback will not get called anymore.
// Likewise, if you change `listener`, such as to add it to a different Signal<int>,
// it will automatically remove it from the original Signal.
// Producer Example
class A
{
public:
int sourceVal;
A(int i) : sourceVal(i) {}
void Modify(int val) {
sourceVal = val;
bloopChanged.raise(val);
}
Signal<int> bloopChanged;
};
// Consumer Example
class B
{
public:
// Like a "slot"
// You can assign the slot to the signal, and it handles disconnect when reset(), or assigned to a different signal.
Signal<int>::Listener bloopResponse;
int x;
B() {
x = 0;
}
~B() {
x = -99;
}
void set(int a) { x = a; }
};
void test() {
A a1(1);
A a2(2);
A a3(3);
// B scope
{
B b;
auto cb = [&](int v) {
b.set(v);
};
b.bloopResponse = a1.bloopChanged.add(cb);
printf("As: (%d, %d, %d) B: %d\n", a1.sourceVal, a2.sourceVal, a3.sourceVal, b.x);
a1.Modify(10);
a2.Modify(20);
a3.Modify(30);
printf("As: (%d, %d, %d) B: %d\n", a1.sourceVal, a2.sourceVal, a3.sourceVal, b.x);
b.bloopResponse = a2.bloopChanged.add(cb);
a1.Modify(11);
a2.Modify(22);
a3.Modify(33);
printf("As: (%d, %d, %d) B: %d\n", a1.sourceVal, a2.sourceVal, a3.sourceVal, b.x);
}
a1.Modify(100);
a2.Modify(200);
a3.Modify(300);
printf("As: (%d, %d, %d) B: (gone)\n", a1.sourceVal, a2.sourceVal, a3.sourceVal);
}
// Prints:
// As: (1, 2, 3) B: 0
// As: (10, 20, 30) B: 10
// As: (11, 22, 33) B: 22
// As: (100, 200, 300) B: (gone)
// Put a breakpoint in the labda, and you will see:
// It only receives: 10, and 22, (it is not fired after leaving "B scope"
// CAUTION:
// This cannot protect against capturing an object in the lambda that goes out of scope.
// If you need to, pass a weak_ptr in your lambda and check it:
// [weak_ptr<YourObj> obj = your_shared_ptr_instance, other_captures]() {
// if (auto o = obj.lock()) {
// // stuff with o
// // ...
// }
// });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment