4
\$\begingroup\$

Implementing signal slot mechanism again seems quite dumb because there are so many people already done this before but it is for learning and I intended some specific goals.

I had some library in mind which is easy and safe to use and relies on C++14 only. The possibility of member function connection and disconnection must be possible too. Achieving normal function connection is easy but I saw a lot of nearly unreadable (but still very well designed) code to achieve member function connection. There is also a need to have a single signal class called an emitter which is able to emit different signals and holds all pointers to the slots.

To achieve this there is the Emitter class which takes variadic template arguments of all signals you want to emit. This implies that you know your signals at compile time. The emitter contains a tuple which holds a SignalContainer where the actual function pointers are stored inside a vector. C++14 offers the ability to get a tuple element by type which is great to manage all possible signals inside a tuple without overblown code.

Connecting signals to slot works with the connect() method of the emitter class. You must pass the signal type as template parameter because the emitter must know in which signal type it has to put the function pointer.

Code

#ifndef EVENT_HPP_
#define EVENT_HPP_

#include <atomic>
#include <tuple>
#include <mutex>
#include <exception>
#include <vector>
#include <utility>

namespace events {
namespace detail {

struct Identifier {
    using id_type = unsigned int;

    static id_type get() {
        static std::atomic<id_type> id(0);
        return id++;
    }
};

template<typename TTuple, typename TFunc, std::size_t N>
struct TupleIterator {

    template<typename ... TParams>
    static constexpr void do_iterate(TTuple &t, TParams ... params) {
        TFunc fn;
        fn(std::get<N>(t), params...);
        TupleIterator<TTuple, TFunc, N-1>::do_iterate(t, params...);
    }
};

template<typename TTuple, typename TFunc>
struct TupleIterator<TTuple, TFunc, 0> {

    template<typename ... TParams>
    static constexpr void do_iterate(TTuple &t, TParams ... params) {
        TFunc fn;
        fn(std::get<0>(t), params...);
    }
};

/** Iterate over a tuple in reverse order because
 * that was simple to implement.
 */
template<typename TFunc, typename TTuple, typename ... TParams>
void iterate_tuple(TTuple &t, TParams ... params) {
    TupleIterator<TTuple, TFunc, std::tuple_size<TTuple>::value - 1>::do_iterate(t, params...);
}

struct DisconnectAll {

    template<typename TContainer>
    void operator()(TContainer &t) {
        t.slots.clear();
    }
};

/** Disconnects an object from a container. The
 * pointer to that object is given.
 */
struct DisconnectObject {

    template<typename TObject, typename TContainer>
    void operator()(TContainer &t, TObject *obj) {
        t.slots.erase(std::remove_if(t.slots.begin(), t.slots.end(), [&](typename TContainer::functional_type &t){
            /* Only delete when the pointer to an object
             * is different from nullptr to prevent deleting
             * non member function slots.
             */
            if(t.getThisPtr() != nullptr && t.getThisPtr() == obj) {
                return true;
            } else{
                return false;
            }
        }), t.slots.end());
    }
};

template<typename TSignal, typename TFunction, typename TObject>
struct FunctionalIdentifier {
    static detail::Identifier::id_type getID() {
        static detail::Identifier::id_type this_id = detail::Identifier::get() + 1; /* Start at 1 because zero is reserved */
        return this_id;
    }
};

/** The functional helper is the actual slot
 * which wraps functions and member functions
 * and keeps a unique identifier to be
 * able to compare which function or member
 * function is stored.
 */
template<typename TSignal>
struct FunctionalHelper {

    FunctionalHelper() {
        this_ptr = nullptr;
        id = 0;
    }

    /** Normal function initialization.
     *
     */
    template<typename TFunction>
    FunctionalHelper(TFunction f) : fn(f), this_ptr(nullptr) {
        id = FunctionalIdentifier<TSignal, TFunction, std::nullptr_t>::getID();
    }

    template<typename TObject>
    FunctionalHelper(TObject *obj, typename TSignal::template member_function_pointer<TObject> ptr) {
        this_ptr = obj;
        id = FunctionalIdentifier<TSignal, decltype(ptr), TObject>::getID();

        /* Use of C++14 generic lambda makes calling the member
         * function so much painless. Unpacking of the tuple happens inside the
         * CallHelper::call function.
         */
        fn = [obj, ptr](auto && ... args){
            return (obj->*ptr)(std::forward<decltype(args)>(args)...);
        };
    }

    template<typename ... TArgs>
    typename TSignal::return_type operator()(TArgs ... args) {
        return fn(std::forward<TArgs>(args)...);
    }

    std::size_t getID() const {
        return id;
    }

    void *getThisPtr() const {
        return this_ptr;
    }
private:
    /** We use the std function here but there
     * is no safe way of comparing functions so we need
     * some more storage variables.
     */
    typename TSignal::std_function_type fn;
    /** Keeping the pointer to an object is
     * required to delete that object later.
     */
    void *this_ptr;
    /** This is an identifier which identifies
     * a signature.
     */
    std::size_t id;
};

}

template<typename TArgs> struct Signal{};

/** This is nothing more than a containr for
 * a type you pass to the template argument list of the emitter
 * so the emitter knows which signal signatures you will
 * provide. There is no need to use this
 */
template<typename TReturn, typename ...TParam>
struct Signal<TReturn(TParam...)> {
public:
    using return_type = TReturn;
    using std_function_type = std::function<TReturn(TParam...)>;
    template<typename TObject>
    using member_function_pointer = return_type (TObject::*)(TParam...);


    detail::Identifier::id_type getID() const {
        static detail::Identifier::id_type this_id = detail::Identifier::get();
        return this_id;
    }

    template<typename TEmitter, typename TFunction>
    static void connect(TEmitter &e, TFunction fn) {
        e.template connect<Signal>(fn);
    }
private:

};

template<class TSignal>
struct SignalContainer {
    using functional_type = typename detail::FunctionalHelper<TSignal>;

    std::vector<detail::FunctionalHelper<TSignal>> slots;
    std::mutex slots_mutex;
};

/** Can emit events for the given event types.
 *
 */
template<class ... TSignals>
class Emitter {
public:

    /**
     * Connect a simple function to a specific signal. Use it the following way:
     * Emitter.connect<SomeSignal>(SomeStaticFunction).
     *
     * A call to this function will not check if the slot function has been registered
     * so a connection of the same function is possible and will raise the handler
     * multiple times when using emit().
     *
     * The return is an identifier which is unique for types. It is not unique
     * for every call to connect().
     */
    template<typename TSignal, typename TFunction>
    std::size_t connect(TFunction fn) {
        auto & container = getContainer<TSignal>();
        std::lock_guard<std::mutex> lock(container.slots_mutex);

        container.slots.emplace_back(detail::FunctionalHelper<TSignal>(fn));
        return container.slots.back().getID();
    }

    template<typename TSignal, typename TObject>
    std::size_t connect(TObject *obj, typename TSignal::template member_function_pointer<TObject> ptr) {
        auto & container = getContainer<TSignal>();
        std::lock_guard<std::mutex> lock(container.slots_mutex);

        container.slots.emplace_back(detail::FunctionalHelper<TSignal>(obj, ptr));
        return container.slots.back().getID();
    }

    /** Disconnects all signals of the given type.
     *
     */
    template<typename TSignal>
    void disconnect() {
        auto & container = getContainer<TSignal>();
        std::lock_guard<std::mutex> lock(container.slots_mutex);

        container.slots.clear();
    }

    /** Disconnect all slots from a given object.
     */
    template<typename TObject>
    void disconnect(TObject *obj) {
        detail::iterate_tuple<detail::DisconnectObject>(signals, obj);
    }

    /** Disconnects everything from the emitter.
     *
     */
    void disconnect() {
        detail::iterate_tuple<detail::DisconnectAll>(signals);
    }

    template<typename TSignal, typename ... TParams>
    void emit(TParams &&...params) {
        auto & container = getContainer<TSignal>();
        std::lock_guard<std::mutex> lock(container.slots_mutex);

        for(auto & it : container.slots) {
            /* Call the function. This does not
             * handle return parameters.
             */
            it(std::forward<TParams>(params)...);
        }
    }
private:
    /** This tuple contains the container
     * for every type which means we can get
     * the container for every specific type.
     * This relies on C++14 feature to get
     * a tuple element by its type.
     */
    std::tuple<SignalContainer<TSignals>...> signals;

    /** Get the signal container for a specific signal
     * type.
     */
    template<typename TSignal>
    SignalContainer<TSignal>& getContainer() {
        return std::get<SignalContainer<TSignal>>(signals);
    }
};

}
#endif /* EVENT_HPP_ */

Example

#include <iostream>

static void test(int i) {
    std::cout << "Received: " << i << std::endl;
}

struct Testclass {
    void test(int i) {
        std::cout << "Received: " << i << std::endl;
    }

    void test1(unsigned int i) {
        std::cout << "Received: " << i << std::endl;
    }
};

struct Testclass1 {
    void test(int i) {
        std::cout << "Received: " << i << std::endl;
    }

    void test1(unsigned int i) {
        std::cout << "Received: " << i << std::endl;
    }
};

int main(int argc, const char **argv) {
    using Signal1 = events::Signal<void(int)>;
    using Signal2 = events::Signal<void(unsigned int)>;

    Testclass tc;
    Testclass1 tc1;
    events::Emitter<Signal1, Signal2> em;

    std::cout << "Connect: " << em.connect<Signal1>(&test) << std::endl;
    std::cout << "Connect: " << em.connect<Signal2>(&tc, &Testclass::test1) << std::endl;
    std::cout << "Connect: " << em.connect<Signal1>(&tc, &Testclass::test) << std::endl;
    em.emit<Signal1>(123);
    em.disconnect(&tc);
    em.emit<Signal1>(123);
    em.disconnect();
    em.emit<Signal1>(123);
}

Notes

It may be useful to have some kind of slot which unregisters itself from classes. Another point is the connect method. You must always provide the signal as template parameter because the method must know what it needs to find inside the tuple. Maybe it could be useful to write a specialized function which checks if the emitter has just a single signal. I don't know how you guys would do that but I'm open to suggestions.

\$\endgroup\$
1
\$\begingroup\$

Missing Include Headers

There are 2 missing include headers, in event.hpp #include <functional> and #include <algorithm> are missing (std::remove_if()). In the example code #include "event.hpp" is missing.

Indentation

This may be a copy and paste problem from your editor to the mark up editor, but namespace detail { and all the code in it should be indented since it is within namespace events {.

To make the code easier to read and maintain it might be better if the detail namespace had it's own header file.

DisconnectObject

Since (t.getThisPtr() != nullptr && t.getThisPtr() == obj) is a boolean expression the following code can be simplified:

        struct DisconnectObject {

            template<typename TObject, typename TContainer>
            void operator()(TContainer &t, TObject *obj) {
                t.slots.erase(std::remove_if(t.slots.begin(), t.slots.end(), [&](typename TContainer::functional_type &t){
                    /* Only delete when the pointer to an object
                     * is different from nullptr to prevent deleting
                     * non member function slots.
                     */
                    if(t.getThisPtr() != nullptr && t.getThisPtr() == obj) {
                        return true;
                    } else{
                        return false;
                    }
                }), t.slots.end());
            }
        };

To

        struct DisconnectObject {

            template<typename TObject, typename TContainer>
            void operator()(TContainer &t, TObject *obj) {
                t.slots.erase(std::remove_if(t.slots.begin(), t.slots.end(), [&](typename TContainer::functional_type &t){
                    /* Only delete when the pointer to an object
                     * is different from nullptr to prevent deleting
                     * non member function slots.
                     */
                    return (t.getThisPtr() != nullptr && t.getThisPtr() == obj);
                }), t.slots.end());
            }
        };

Constructors for FunctionalHelper Inconsitently use Initializers

There are 3 different constructors for FunctionalHelper On doesn't use initializers at all, but could consist of only initializers

            FunctionalHelper() {
                this_ptr = nullptr;
                id = 0;
            }

Could be refactored as

            FunctionalHelper()
            : this_ptr{nullptr}, id{0}
            {
            }

            template<typename TObject>
            FunctionalHelper(TObject *obj, typename TSignal::template member_function_pointer<TObject> ptr) {
                this_ptr = obj;
                id = FunctionalIdentifier<TSignal, decltype(ptr), TObject>::getID();

                /* Use of C++14 generic lambda makes calling the member
                 * function so much painless. Unpacking of the tuple happens inside the
                 * CallHelper::call function.
                 */
                fn = [obj, ptr](auto && ... args){
                    return (obj->*ptr)(std::forward<decltype(args)>(args)...);
                };
            }

Could be refactored as

            template<typename TObject>
            FunctionalHelper(TObject *obj, typename TSignal::template member_function_pointer<TObject> ptr)
            : this_ptr {obj}
            {
                id = FunctionalIdentifier<TSignal, decltype(ptr), TObject>::getID();
                fn = [obj, ptr](auto && ... args){
                    return (obj->*ptr)(std::forward<decltype(args)>(args)...);
                };
            }

The functions std::size_t getID() and void *getThisPtr() are never used, this could indicate that the variables id and this_ptr are never used. Unused functions and unused variables can lead to problems during maintenance.

The variable this_ptr is a raw pointer, perhaps a smart pointer should be used?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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