I programmed in C# and I used the Qt framework. Both of them have their own signal slot mechanism which are really powerful. I looked at several implementations in C++ and based on what I learnt from those sources, I started to implement my own version to better understand how these could work under the hood.
My implementation currently has no support for accessing the return values of the slots, and provides no thread safety. I implemented an auto disconnecting mechanism to manage the slots life cycle and disconnect them if those go out of scope. I posted the source code on GitHub. I've used std::function
and variadic templates.
You declare a signal this way:
signal<int> test_signal; // test_signal has an int parameter
You can connect slots with the connect function:
test_signal.connect(&slot_function); // void slot_function(int)
// Connecting to a member function, f is the instance of the object
test_signal.connect(&foo::slot_function, f); // void foo::slot_function(int)
You can connect multiple slots to a signal which are stored in a vector. Every connect method returns a connection class which has a disconnect function which disconnects the slot from the signal. The foo object must inherit from auto_disconnect class for life cycle management. This class stores a vector of connections and when it's destructed it disconnect all of the slots. If you want to emit a signal you can use the overloaded operator()
.
test_signal(1);
I never done anything like this before and I wanted to spot any design errors, make it more efficient, and have a better understanding about this.
Here is the full source code with example usage:
//signal.h
#ifndef SIGNAL_H__
#define SIGNAL_H__
#include <functional>
#include <vector>
namespace szabi
{
// When a slot is overloaded an ugly cast should be used
// static_cast<void(T::*)(<parameters>)>(&T::<slot>);
// This helper simplifies this a bit
// overload<parameters>::of(&T::slot);
template <typename... Args>
struct overload
{
template <typename F, typename R>
static constexpr auto of(R(F::*ptr)(Args...)) -> decltype(ptr)
{
return ptr;
}
};
// This class manages a connection between a signal and slot
class connection
{
public:
connection(std::function<void()>&& disconnector) : disconnector(std::move(disconnector)) {}
~connection() {}
void disconnect() const
{
this->disconnector();
}
private:
// This will contain a lambda function which will remove the slot from the signal's vector
std::function<void()> disconnector;
};
// This class is used to disconnect all slots when the object is destroyed
class auto_disconnect
{
// A signal has access to add_connection function
template<typename... Args> friend class signal;
public:
virtual ~auto_disconnect()
{
for(const auto& conn : this->connections)
{
conn.disconnect();
}
}
protected:
void add_connection(const connection& conn)
{
this->connections.push_back(conn);
}
private:
std::vector<connection> connections;
};
template <typename... Args>
class signal
{
public:
signal() {}
virtual ~signal() {}
using slot_type = std::function<void(Args...)>;
using container_type = std::vector<slot_type>;
using iterator_type = typename container_type::iterator;
template <typename S>
connection connect(S&& slot)
{
this->slots.push_back(std::forward<S>(slot));
connection conn =
{
[&]()
{
iterator_type it = std::prev(this->slots.end());
this->slots.erase(it);
}
};
return conn;
}
template <typename S, typename T>
connection connect(S&& slot, T* instance)
{
static_assert(std::is_base_of<auto_disconnect, T>::value, "T must inherit auto_disconnect");
this->slots.emplace_back([&](Args... args)
{
// Using a lambda function to call a slot which is a member function
(static_cast<T*>(instance)->*slot)(std::forward<Args>(args)...);
});
connection conn =
{
[&]()
{
iterator_type it = std::prev(this->slots.end());
this->slots.erase(it);
}
};
instance->add_connection(conn);
return conn;
}
template <typename S, typename T>
connection connect(S&& slot, T& instance)
{
static_assert(std::is_base_of<auto_disconnect, T>::value, "T must inherit auto_disconnect");
return this->connect(slot, std::addressof(instance));
}
void operator()(Args const&... args)
{
for(auto const& slot : this->slots)
{
if(slot)
{
slot(args...);
}
}
}
private:
container_type slots;
};
}
#endif /* SIGNAL_H__ */
// main.cpp
#include "signal.h"
#include <iostream>
#include <cstdlib>
void func(int x)
{
std::cout << "void func(" << x << ")" << std::endl;
}
class foo : public szabi::auto_disconnect
{
public:
foo() {}
~foo() {}
void func(int x)
{
std::cout << "void foo::func(" << x << ")" << std::endl;
}
void func(int x, int y)
{
std::cout << "void foo::func(" << x << ", " << y << ")" << std::endl;
}
void func0()
{
std::cout << "void foo::func0()" << std::endl;
}
void func1(int x)
{
std::cout << "void foo::func1(" << x << ")" << std::endl;
}
void func2(int x, int y)
{
std::cout << "void foo::func2(" << x << ", " << y << ")" << std::endl;
}
};
class bad : public szabi::auto_disconnect
{
public:
bad()
{
std::cout << "bad constructed" << std::endl;
}
~bad()
{
std::cout << "bad destructed" << std::endl;
}
void func(int)
{
std::cout << std::endl << "void bad::func()" << std::endl << std::endl;
}
};
int main()
{
szabi::signal<> signal0;
szabi::signal<int> signal1;
szabi::signal<int, int> signal2;
foo f;
signal0.connect(&foo::func0, f);
signal1.connect([](int x)
{
std::cout << "Lambda func, x = " << x << std::endl;
});
signal1.connect(&func);
// overloaded slots
signal1.connect(static_cast<void(foo::*)(int)>(&foo::func), f);
signal2.connect(static_cast<void(foo::*)(int, int)>(&foo::func), f);
// using overload<...>::of(&foo::bar) helper
signal1.connect(szabi::overload<int>::of(&foo::func), f);
signal2.connect(szabi::overload<int, int>::of(&foo::func), f);
signal1.connect(&foo::func1, f);
signal2.connect(&foo::func2, f);
{
bad b;
szabi::connection conn = signal1.connect(&bad::func, b);
}
signal0();
signal1(1);
signal2(1, 2);
return EXIT_SUCCESS;
}