3
\$\begingroup\$

I'm relatively new to C++ (learned it years ago, but never used it much until now), and I needed an exercise to get a better handle on the language. I also needed a good way of handling events, and while there are plenty of libraries that have good event systems, I didn't like the client code for any of them. It felt unintuitive and like the listeners were having to do a lot more than just "listen." So I decided to try my hand at making my own, with pretty specific client code in mind as a goal.

There's obviously some flaws in the code still, and some things that I'm not sure about (how to apply the Law of Three to the Listener class?), but overall the code works exactly how I wanted it to, so I'm pretty satisfied. Since it's my first real project in C++ though, I'd really love any input and critique I could get on it though.

The Requirements

First and foremost, I wanted the system to be able to have callbacks of any type. Lambdas (with or without delegating to a member function), free functions, static functions, or member functions bound with std::bind. And I didn't want the listener to have to specify what kind of callback they were registering, or the event dispatcher to have to have any knowledge of what type of callback it was. I wanted the system to work exactly the same regardless of the kind of callback.

This introduced the next requirement. Lifetime handling, and the problems that came with passing delegates because of it. The undefined behavior from having a delegate get called for a deleted object would be a nightmare. So the listeners needed to be automatically disconnected from the event when they were destroyed, but be able to disconnect at a specific time if needed. And I didn't want them to have to worry about whether the object they had previously registered to was destroyed already, before disconnecting from it.

Event dispatchers needed to be able to have as many events as needed, without having to create a new event class extending from an abstract base for every type. I'd dealt with that in java before and it gets ugly real fast when you've got dozens of classes, all firing different types of events. But at the same time, they needed to be able to pass specific information about the event to the callback function.

I also didn't want the event dispatcher to have anything to do with handling the actual listeners. Not connecting, not disconnecting, not checking if the functions could still be called, nothing. Just defining the type of event they have, and firing it at the appropriate time. Additionally, I didn't want the event dispatcher to have to inherit a class and implement its methods to make the system work.

Next, I wanted to be able to register a listener both inside and outside the listener's class. While this introduces some issues if you pass a delegate to a different object as the callback, it was more important for me to be able to register listeners on the fly.

And lastly, and kind of as an afterthought, I wanted to be able to mute events so that none of the callbacks fired, and temporarily disable specific callbacks, but leave the rest still firing.

The Classes

  • Connection and TypedConnection: Essentially a handle and/or proxy between the Event and the Listener, so that neither has to worry about whether the other is still alive, and can disconnect freely at any point. It's passed through shared pointers to ensure that it will remain intact to be disconnected from as long as either the Event or the Listener are still around.

  • Event: Pretty straightforward. A template class to allow the dispatcher to pass whatever parameters it needs to the Listener, depending on what type of event it is. It handles the the Connection to the Listener so that the class triggering the event doesn't have to.

  • Listener: Can either be inherited by the class wanting to hook into an event, or used through composition if necessary. It handles disconnecting the Connection automatically so that the Event can never call a Listener that's been destroyed. Only this class can create a Connection instance, to ensure that it will be handled properly based on the lifetime of the object.

Source Code

Keep in mind things aren't split up into their final files yet. I'm aware that everything shouldn't be implemented in Events.h.

#ifndef EVENTS_H
#define EVENTS_H

#include <functional>
#include <memory>
#include <vector>
#include <algorithm>

class Connection;
template<typename... Arguments>
class TypedConnection;
template<typename... Arguments>
class Event;
class Listener;

// Wrapper class for TypedConnection to be able to reference them without knowing the callback parameters, and store Connections for any type of event in one vector
class Connection {
public:
    virtual bool isConnected() const = 0;
    virtual void disconnect() = 0;
    virtual void pause() = 0;
    virtual void unpause() = 0;
};


template<typename... Arguments>
class TypedConnection : public Connection {
public:
    friend class Listener;

    bool isConnected() const {
        return this->isConnected_;
    }

    void runCallback(Arguments... arguments) {
        if ((!this->isPaused) && this->isConnected_) {
            this->callback(arguments...);
        }
    }

    //Permanently disconnects the Listener from the event
    void disconnect() {
        this->isConnected_ = false;
    }

    //Temporarily stops the callback from being fired until the connection is unpaused (might be better named block()?)
    void pause() {
        this->isPaused = true;
    }

    void unpause() {
        this->isPaused = false;
    }

private:
    std::function<void(Arguments...)> callback;
    // Is there a better naming convention that keeps the is/can/has names for booleans, but doesn't conflict with their accessors?
    bool isConnected_ = true;
    bool isPaused = false;

    // Constructor is private so that only a Listener class can create a connection, ensuring the connection will be disconnected at the end of the listeners lifetime
    TypedConnection(std::function<void(Arguments...)> callback) : callback(callback) {}
};


// An Event object is created for every type of event a class can fire, and saved as a member variable of the event dispatcher
// Might rename EventSource or EventHandler or something along those lines
template<typename... Arguments>
class Event {
public:
    using ConnectionHandle = std::shared_ptr<TypedConnection<Arguments...>>;
    using Connections = std::vector<ConnectionHandle>;

    void connect(ConnectionHandle connection) {
        this->connections.push_back(connection);
    }

    // It was originally a design flaw that this function could be called outside of the class that owned the Event
    // but after remembering how many times I've used the .trigger() function in jQuery, I decided to keep it public
    void trigger(Arguments... arguments) {
        clearDisconnected();
        if (!this->isMuted) {
            for (ConnectionHandle connection : connections) {
                connection->runCallback(arguments...);
            }
        }
    }

    // Temporarily stops callbacks from being fired on an event
    void mute() {
        this->isMuted = true;
    }

    void unmute() {
        this->isMuted = false;
    }

private:
    Connections connections;
    bool isMuted = false;

    void clearDisconnected() {
        this->connections.erase(std::remove_if(connections.begin(), connections.end(), [](ConnectionHandle connection) {
            return !connection->isConnected();
        }), connections.end());
    }
};


// Simple wrapper to allow automatic type conversion between callbacks of any type and std::function within a template function
// Would it be better to make it an inner class of Listener?
template<typename T>
struct Callback {
    typedef T type;
};

// Does the Rule of Three/Five apply here? If so, what would need to be done on copy/move? Disconnect all?
// Listener class can be created through inheritance (my personal preference) OR through composition
class Listener {
public:
    virtual ~Listener() {
        this->disconnectAll();
    }

    // Returns a shared pointer to allow that specific connection to be handled (disconnect, pause, etc.) at a time other than object destruction
    // I believe function can be treated like a void return type otherwise? Or is the returned value not automatically discarded in that case?
    template<typename... Arguments>
    std::shared_ptr<Connection> connect(Event<Arguments...>& event, typename Callback<std::function<void(Arguments...)>>::type callback) {
        // Have to use new instead of make_shared here because of TypedConnnection is only friends with Listener, so it has to be created here
        std::shared_ptr<TypedConnection<Arguments...>> connection(new TypedConnection<Arguments...>(callback));
        this->connections.push_back(connection);
        event.connect(connection);
        return connection;
    }

    void disconnect(std::shared_ptr<Connection> connection) {
        connection->disconnect();

        // Removes all disconnected connections, to remove any that might have been disconnected directly from the connection pointer
        // Would it be better design to make Connection.disconnect() private and force connections to be disconnected through the Listener?
        connections.erase(
                std::remove_if(connections.begin(), connections.end(), [](std::shared_ptr<Connection> connectionPtr) {
                    return connectionPtr->isConnected();
                }), connections.end());
    }

protected:
    // Might change this to public... not sure yet whether that would be safe or not
    void disconnectAll() {
        for (auto connection : this->connections) {
            connection->disconnect();
        }
        connections.clear();
        std::vector<std::shared_ptr<Connection>>().swap(connections);
    }

private:
    // Stored as shared_ptr to ensure that disconnect can be called regardless of the lifetime of the event dispatcher
    std::vector<std::shared_ptr<Connection>> connections;
};

#endif

Example

#include <iostream>
#include "Events.h"

using std::cout;
using std::endl;
using std::string;

class Cat {
public:
    void meow() {
        cout << "The cat meows" << endl;
        meowEvent.trigger();
    }

    Event<>& getMeowEvent() {
        return this->meowEvent;
    }

private:
    // I believe it also works to create a static event to hook into the event on any instance of the class
    Event<> meowEvent;
};

class Dog : public Listener {
public:
    Dog(string name) : name(name) {}
    void bark() {
        cout << name << " barks" << endl;
    }
private:
    string name;
};

int main() {
    // Basics
    Cat cat;
    Dog toto("Toto");
    Dog lassie("Lassie");

    // Can also be passed a free function, static function, std::bind delegate, or non-delegate lambda
    auto lassieConnection = lassie.connect(cat.getMeowEvent(), [&lassie]() {lassie.bark();});
    toto.connect(cat.getMeowEvent(), [&toto]() {toto.bark();});

    cat.meow();
    cout << endl;
    /* Output:
     * The cat meows
     * Lassie barks
     * Toto barks
     * */


    // Connection pausing
    lassieConnection->pause();
    cat.meow();
    cout << endl;
    /* Output:
     * The cat meows
     * Toto barks
     * */

    lassieConnection->unpause();
    cat.meow();
    cout << endl;
    /* Output:
     * The cat meows
     * Lassie barks
     * Toto barks
     * */


    // Event muting
    cat.getMeowEvent().mute();
    cat.meow();
    cout << endl;
    /* Output:
     * The cat meows
     * */

    cat.getMeowEvent().unmute();
    cat.meow();
    cout << endl;
    /* Output:
     * The cat meows
     * Lassie barks
     * Toto barks
     * */

    return 0;
}
\$\endgroup\$
3
\$\begingroup\$

If you have a class where it doesn't semantically make any sense for it to be copied (or moved), you can tell the compiler not to generate these constructors (or assignment operator overloads) by marking them as = delete; in the class body.

So e.g. Listener::Listener(Listener& other) = delete;

\$\endgroup\$
  • \$\begingroup\$ I was aware that those constructors/assignments could be deleted, but I'm not sure whether it semantically makes sense for the listener to be copied/moved or not. Currently the system doesn't handle it properly, especially with delegate callbacks, but I'm not sure if that's a flaw in the design or something I just need to disallow. \$\endgroup\$ – Azk Oct 13 '16 at 23:53
  • 1
    \$\begingroup\$ @Azk If the system doesn't handle it properly yet, you should disallow it. It's easy to allow it later, but hard to disallow when someone ends up accidentally using the copy constructor \$\endgroup\$ – Justin Oct 15 '16 at 20:38

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.