Click here to Skip to main content
15,881,248 members
Articles / Programming Languages / C++

Yassi: Yet Another Signal/Slot Implementation

Rate me:
Please Sign up or sign in to vote.
4.90/5 (16 votes)
18 Jan 2015CPOL11 min read 40.3K   583   29   23
A C++11 template-based signal/slot library

Introduction

If you've ever used Qt to build a GUI, you're probably familiar with their signal/slots implementation. For me, it was my first encounter with the idiom and I really liked it. The design made me feel like I could have different elements interact with eachother without them even being aware of eachother's existence! Of course, this wasn't true... The Qt macro's just hide the details.

Where Qt uses macro's to build a run-time lookup mechanism, this implementation uses templates (and therefore lots of Template Metaprogramming (TMP)) to construct as much as possible at compile-time. I'm aware that this is not the first implementation that does so, hence the name.

Yassi is very flexible and allows you to connect signals to all possible function-like objects in a type-safe manner:

  • free functions
  • member functions
  • functors
  • lamda's

Thanks to variadic templates, there is no limitation to the number of parameters a slot can take. Also, as a result of all this template-based design, the implementation does not need any virtual functions, so there's no virtual dispatch overhead.

Update (22/01/2015):

  • Fixed a big in SafeSlot
  • Got it to compile on Visual Studio 2013!

Update (21/01/2015):

The latest version features the following improvements:

  • Possibility to connect signals to any object, iclududing temporary functors/lambda's and const objects
  • A SafeSlot base-class for slots, that will disconnect itself from emitters on destruction
  • Better functor behavior, allowing overloaded function-call operators.

Also, I have gotten rid of many of the static_assert() messages. These might have been helpful, but complicated the underlying codebase.

Background

What are signals and slots?

In case you were wondering what signals and slots even are, I'll explain the concept very briefly. Slots are just function-objects that are called when a certain signal is "emitted". A typical signal/slot library provides an API to connect, disconnect, and emit these signals, thereby providing an intuitive way of managing callbacks that should happen when certain events occur. This has proved very useful in GUI design, where different elements need to "talk" to eachother. This is a difficult problem, because the programmer has to make every element visible to every other element in order for them to communicate. The signal/slot idiom simplifies this. For example, consider a GUI that has a PushButton and a TextBox object. One might do the following (just ignore the syntax for now):

C++
button.connect<ButtonClicked>(textbox, &TextBox::display);

Whenever the button is clicked, it should now emit the ButtonClicked signal, possibly with an argument, and display will be called with this argument

emit<ButtonClicked>("Click!"); // display the "Click!" in the textbox

Compilation

This is a one-header library without any dependencies other than the STL. It should therefore be easy to integrate into any project. I have tested the code on several versions of two modern C++11 compilers, both on Linux: GCC 4.8, GCC 4.9, Clang 3.4 and Clang 3.5. The code should compile without warnings at least on those compilers.  Just remember to compile with --std=c++11!

Update: As of 21/01/2015, it also compiles on Microsoft Visual Studio 2013 and whatever version of MSVC that uses.

Porting to C++98 is, I think, a no-go. The code just relies too heavily on handy features like std::function, std::bind, variadic templates and the many type-traits added to the STL since C++11.

Usage

Hello World

How else to start than with a simple "Hello World" example:

C++
#include <iostream>
#include "yassi.h"
using namespace yassi;
using namespace std;

// Declare the signals:
using Event1 = Signal<void()>;
using Event2 = Signal<void(string const &)>;

// Define a class that is able to emit both Event1 and Event2:
class SomeClass: public Emitter<Event1, Event2>
{};

void sayHello()
{
    std::cout << "Hello";
}

void say(string const &msg)
{
    std::cout << msg;
}

int main()
{
    SomeClass emitter;
    emitter.connect<Event1>(sayHello);
    emitter.connect<Event2>(say);

    emitter.emit<Event1>();
    emitter.emit<Event2>(" World!\n");
}

The following sections will explain why exactly this program will print "Hello World!" to the console output.

Declaring Signals

The Signal<> class-template takes a function-signature as a template-parameter. This simply means that this specific signal can be connected to any function-like object with the same function-signature. It's always a good idea to have descriptive names for your types and variables, so I'm using the C++11 type-alias syntax (basically another way of typedeffing) to declare all the signals I'm going to use. In the real world, these might be things like ButtonClicked, KeyPressed, etc.

The Emitter<> base-class

The other libraries that I looked into (libsigc++, boost::signal, boost::signal2) all use some kind of signal-object that is connected to slots, which are called when certain member of the signal-object is called. I chose a slightly different method by defining a base-class-template called Emitter<> from which you can derive any class that needs to emit signals (or you could use it directly of course). The signals that an emitter is able to emit must be passed as template-parameters to Emitter<>.  This can be a subset of all available signals, in which case it won't be possible for this particular emitter to emit the excluded signals. In the "Hello World"-example, SomeClass is able to emit both Event1 and Event2. Its body is left empty, so we could just as well have used the Emitter class without deriving from it.

Connecting Signals to Slots

In the main() function, we connect both Event1 and Event2 to slots that match their respective signatures. For example, we connect Event1, which is an alias for Signal<void()> to sayHello, which also has signature void().

Emitting Signals

After the signals are connected, we call emit on the emitting object, using the signal again as a template argument. This has the effect of calling every slot connected to this signal in the order in which they have been connected. In this example, both Event1 and Event2 have only one slot connected to them. Because Event2 is connected to functions that require an function-argument, this argument has to be specified to emit.

Connecting signals to other stuff

Connecting to Member Functions

When connecting a signal to member functions, an object is required to be passed to connect(), along with the member function that has to be called on this object. For example:

C++
using IntegerEvent = Signal<void(int)>;

class EventHandler
{
    int d_x{1};

public:
    void handle(int x)
    {
        cout << "New value: " << (d_x += x) << '\n';
    }
};

int main()
{
    Emitter<IntegerEvent> emitter; // Note: using it directly for simplicity
    EventHandler handler;

    emitter.connect<IntegerEvent>(handler, &EventHandler::handle);
    emitter.emit<IntegerEvent>(3); // prints "New value: 4"
}

The requirement when connecting to member-functions however, is that the object must still be alive when the signal is emitted. Therefore, it is impossible to connect a signal to a temporary object and its member. It's up to the user to make sure the object stays alive for long enough. The object can be safely deleted only after it has been disconnected from all emitters. For example, the following code will compile but has undefined behavior:

void screwUp(Emitter<Event> &em)
{
    EventHandler handler;
    em.connect<Event>(handler, &EventHandler::handle);

    // Oh no! The handler-object goes out of scope...
} 

int main()
{
    SomeEmittingClass emitter;
    // emitter.connect<Event>(Handler(), &EventHandler::handle); // won't compile!

    screwUp(emitter);
    emitter.emit<Event>(); // tries to call handle() on a dead object...
}

A neat solution to the problem above is using the SafeSlot base-class (see below).

It is also possible to connect signals to static members, in which case the object must be omitted from the call to connect (i.e. just like a free function):

connect<Event>(&Class::staticMember);

Overloaded Functions

When you want to use an overloaded function as a slot, you have to be more specific in order for the compiler to be able to select the right overload. For example, when dealing with overloaded free functions:

void overloaded(int) {} // 1
int overloaded() {}     // 2

// ...

connect<Signal>(overloaded); // error: ambiguous overload
connect<Signal>(static_cast<void(*)(int)>(overloaded)); // use #1
connect<Signal>(static_cast<int(*)()>(overloaded));     // use #2

For overloaded member-functions, the syntax is a little more compilicated, because we're dealing with member-function pointers, but the principle is exactly the same:

connect<Signal>(object, static_cast<void(Class::*)(int)>(&Class::overloaded));

Connecting to Functors/Lambda's

When you need a function to preserve some state, functors or lambda's (compiler-generated functors) are a much used solution. Therefore, signals can be connected to functors, much like they can be connected to free functions. Unlike normal objects, temporary functors can be connected to signals. When you choose to bind a temporary, the restriction is that you provide a second template int-parameter as a label, in order to be able to disconnect it again using this same label.

C++
using Event = Signal<void()>;

struct Functor
{
    void operator()() // make sure the signal matches this signature
    {
        cout << "Hello from the functor!\n";
    } 
};

int main()
{
    Emitter<Event> emitter;
    Functor functor;

    emitter.connect<Event>(functor);
    emitter.connect<Event, 1>([] { cout << "Hello from the lambda!\n"; });

    emitter.emit<Event>();
}

Overloading operator() of a functor

The functioncall-operator of a functor can be overloaded as much as you like. Based on the signal-signature and the const-ness of the functor object (temporaries being treated as non-const), the compiler will try to select the correct overload. This also has implications for constant functors. For example:

class Functor
{
public:
    void operator()();          // 1
    void operator()() const;    // 2
    void operator()(int);       // 3
    void operator()(int) const; // 4
};

// ...

using Event1 = Signal<void()>;
using Event2 = Signal<void(int)>;

Functor f1;
Functor const f2;

connect<Event1>(f1); // select #1
connect<Event1>(f2); // select #2
connect<Event2>(f1); // select #3
connect<Event2>(f2); // select #4

connect<Event1, 1>(Functor()); // select #1
connect<Event2, 2>(Functor()); // select #3

Disconnecting Signals

Of course it's possible to disconnect signals from slots in quite the same way as you would connect them, but with the opposite effect. You can choose to specify which signal to disconnect from, or just disconnect from all signals.

// Free functions and static members:
connect<Signal>(function);
disconnect<Signal>(function);         // disconnect function from Signal
disconnect(function);                 // disconnect function from all signals

// Member functions
connect<Signal>(object, function);
disconnect<Signal>(object, function); // undoes this particular connection 
disconnect<Signal>(object);           // all slots of object from Signal
disconnect<Signal>(function);         // all objects that have this slot connected
disconnect<Signal>();                 // everything connected to Signal
disconnect(...);                      // same arguments as above, for all signals

// Functors/Lambda's
connect<Signal>(functor);             
disconnect<Signal>(functor);          // disconnect functor from Signal
disconnect(functor);                  // disconnect functor from all signals

// Temporary functors/lambda's
connect<Signal, Label>(Functor());
disconnect<Signal, Label>();          // disconnect Signal from everything with this label
disconnect<Label>();                  // disconnect every signal from this label

// Disconnect everything
disconnect();                         // break every single connection

It is possible to connect the same signal multiple times to the same slot (on the same object), or the same label. Disconnecting again normally means thta only one of these connections will be broken. However, when you disconnect from e.g. a member function without specifying the object, every matching connection will be broken including duplicates:

connect<Signal>(object, &Class::member);
connect<Signal>(object, &Class::member);

disconnect<Signal>(object, &Class::member); // will break only 1 connection
disconnect<Signal>(object);                 // will break all connections
disconnect<Signal>(&Class::member);         // will break all connections

I would recommend that you be as explicit as you possibly can be when disconnecting signals. It's quite easy to break more connections than you intended if you're not careful.

Return value of disconnect()

Every call to disconnect() returns a boolean that tells you whether or not anything has been disconnected. This can be used for example in a loop to break duplicate connections:

while (emitter.disconnect<Event>(func))
    ; // breaks every connection to func on Event

Return Values

It is possible to delare signals with return-values. These values will all be returned to the caller of emit() in a std::vector<T> (where T denotes the return-type). For example:

using EventWithReturn = Signal<int(int)>;

template <int X>
int times(int x)
{
    return x * X;
}

int main()
{
    Emitter<EventWithReturn> emitter;    

    emitter.connect<EventWithReturn>(times<2>);
    emitter.connect<EventWithReturn>(times<3>);
    emitter.connect<EventWithReturn>(times<4>);

    for (int i: emitter.emit<EventWithReturn>(2))
        cout << i << '\n';
}

/* Output:
    4
    6
    8     */ 

This might not be desirable in performance-applications, which is why you can always opt to back away from constructing and returning the vector of return-values by passing void as a second argument to emit():

emitter.emit<Event, void>(2); // won't return anything

Disambiguating Signals

You might need different signals with the same type. In this case you need to disambiguate them by passing a second (integer) argument which is different for every event:

using Event1 = Signal<void(int), 1>;
using Event2 = Signal<void(int), 2>;

// Note: Signal<void(int)> == Signal<void(int), 0>

The SafeSlot base-class

Again, it's not safe to deallocate objects before you disconnect them. This leads to internal dangling pointers and therefore undefined behavior. The SafeSlot class-template aims to make it somewhat easier for the programmer to manage connections. Classes derived from SafeSlot will disconnect themselves from the emitter(s) upon destruction. For example:

C++
class Slot: public SafeSlot
{
public:
    using SafeSlot::SafeSlot; // Inheriting the constructor

    void slot() const
    {
        std::cout << "Better safe than sorry!\n"
    }
};

This trivial example inherits the constructor of its base-class for simplicity, but this hides some interesting details. Also, this is no longer possible when the Slot class needs to initialize some members itself using a constructor. Probably a better use-case example:

C++
class Slot: public SafeSlot
{
    int d_state;

public:
    template <typename ... Args>
    Slot(int state, Args&& ...args):
        SafeSlot(std::forward<Args>(args)...),
        d_state(state)
    {}

    void slot() const
    {
        cout << "state: " << d_state << '\n';
    }
};

We can see now that the SafeSlot constructor takes any number of arguments of any type. These arguments should be of type Emitter<>. In fact, SafeSlot is an emitter itself. In the constructor, it connects itself to each of the emitter-objects that were passed. In its destructor, it emits a signal that tells all the emitters that it wants to be disconnected from them.

To demonstrate its use, try to predict what the output of the following program should be:

C++
class Slot
{
    int d_state;
public:
    Slot(int state):
        d_state(state)
    {}

    void incr(int x) { d_state += x; }
    void show() const { cout << "state: " << d_state << '\n'; }
};

using Increment = Signal<void(int)>;
using Show = Signal(void()>;

int main()
{
    Emitter<Increment, Show> em;

    {
        Slot s1(3); 
        em.connect<Increment>(s1, &Slot::incr);
        em.connect<Show>(s1, &Slot::show);

        em.emit<Increment>(2);
        em.emit<Show>();
    }  // s1 destroyed

    Slot s2(8); 
    em.connect<Increment>(s2, &Slot::incr);
    em.connect<Show>(s2, &Slot::show);

    em.emit<Increment>(2);
    em.emit<Show>();
}

If you had trouble figuring out what the output would be, you got the point. There's no way of being certain, because signals are being emitted to an object that has been destroyed:

GCC 4.9
  state: 5
  state: 12
  state: 12

Clang 3.5
  state: 5
  state: 7
  state: 10

Now, using the SafeSlot base-class:

C++
class Slot: public SafeSlot
{
    int d_state;
public:
    template <typename ... Args>
    Slot(int state, Args&& ... args):
        SafeSlot(std::forward<Args>(args) ...),
        d_state(state)
    {} 

    void incr(int x) { d_state += x; }
    void show() const { cout << "state: " << d_state << '\n'; }
};

int main()
{
    Emitter<Increment, Show> em;

    {
        Slot s1(3, em); // NOTE: have to pass the relevant emitters now
        em.connect<Increment>(s1, &Slot::incr);
        em.connect<Show>(s1, &Slot::show);

        em.emit<Increment>(2);
        em.emit<Show>();
    }  // s1 destroyed and disconnected from em

    Slot s2(8, em); 
    em.connect<Increment>(s2, &Slot::incr);
    em.connect<Show>(s2, &Slot::show);

    em.emit<Increment>(2);
    em.emit<Show>();
}

With a little effort, our slot-class is now safe to use. The program prints:

state: 5
state: 10

In case the slot is connected to emitters that are not accessible at construction, you can add emitters to the SafeSlot using its addEmitters() member (which is itself a variadic template accepting multiple emitters at once):

Slot slot(3, em); 

// is equivalent to

Slot slot(3);
slot.addEmitters(em);

Debugging

It's only human to make mistakes, so this will probably happen too when using this library. Because of the heave use of templates and TMP in the implementation, the resulting error-messages can be frightning. If you encounter long indecipherable error-messages, check the following:

  1. Does the signal match the slot-signature when (dis)connecting a signal?
  2. Is the function really a member of the object when (dis)connecting member functions?
  3. Are you trying to connect a temporary object which is not a functor?
  4. Did you specifiy a label when connecting a temporary functor or lambda?
  5. Did you try to connecting a signal to a const object and a non-const member function?
  6. Did you try to connect a const functor that does not provide a const overload of the appropriate function-call operator?
  7. Is the signal you're trying to emit in the list of signals passed to Emitter<>?
  8. When calling emit(), are you passing arguments that match the signal-signature?
  9. Are the functor-slots declared in the public section?
  10. Member-function slots should be public, unless they are being connected from within a member-function of the slot-owner (the statement &Class::privateMember is only allowed from within Class itself).
  11. .... probably more ....

Of course, there may still be bugs in my implementation. If so, please provide me with a piece of sample-code (shorter == better) and the compiler error-message.

Points of Interest

Please let me know if anyone is interested in a detailed article on the implementation. It involves a lot of TMP to get the job done, and has certainly been another great learning experience to me. If there is enough interest in a follow-up article like this, I will consider writing one.

Contact

If you're interested in contacting me, please use the comment-system or email me at
jorenheit [at] gmail [dot] com

Thanks for Reading!

History

  • Jan. 19: First Draft
  • Jan. 19: Added background including working compilation environments. Fixed some errors in yassi.h because it wouldn't compile on Clang. Some other minor edits.
  • Jan. 20: Major code-revision to allow for constant objects. Better diagnostics.
  • Jan. 20: Source code cleaned up a little: removed the ugly const-cast. Better functor behavior.
  • Jan. 21: Major revision of the source code. Allow for temporary functors and lambda's now. Added SafeSlot.
  • Jan. 21: Made sure emit() is callable on const objects. Let disconnect()return a boolean.
  • Jan. 22: Code now compiles on Visual Studio 2013.
     

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 4 Pin
Linuxjca Jca7-Aug-22 21:35
Linuxjca Jca7-Aug-22 21:35 
Questionthreadsafe? Pin
Member 115263389-Apr-15 4:53
Member 115263389-Apr-15 4:53 
QuestionPorting to VC++ 12 Pin
geoyar19-Jan-15 15:11
professionalgeoyar19-Jan-15 15:11 
AnswerRe: Porting to VC++ 12 Pin
Joren Heit19-Jan-15 15:26
Joren Heit19-Jan-15 15:26 
GeneralRe: Porting to VC++ 12 Pin
geoyar19-Jan-15 16:00
professionalgeoyar19-Jan-15 16:00 
GeneralRe: Porting to VC++ 12 Pin
Joren Heit19-Jan-15 16:26
Joren Heit19-Jan-15 16:26 
GeneralRe: Porting to VC++ 12 Pin
geoyar20-Jan-15 16:12
professionalgeoyar20-Jan-15 16:12 
GeneralRe: Porting to VC++ 12 Pin
Joren Heit20-Jan-15 16:28
Joren Heit20-Jan-15 16:28 
GeneralRe: Porting to VC++ 12 Pin
geoyar21-Jan-15 10:22
professionalgeoyar21-Jan-15 10:22 
GeneralRe: Porting to VC++ 12 Pin
Joren Heit21-Jan-15 10:57
Joren Heit21-Jan-15 10:57 
GeneralRe: Porting to VC++ 12 Pin
Joren Heit22-Jan-15 0:35
Joren Heit22-Jan-15 0:35 
GeneralRe: Porting to VC++ 12 Pin
alexquisi22-Jan-15 6:31
alexquisi22-Jan-15 6:31 
GeneralRe: Porting to VC++ 12 Pin
Joren Heit22-Jan-15 6:42
Joren Heit22-Jan-15 6:42 
GeneralRe: Porting to VC++ 12 Pin
geoyar22-Jan-15 16:32
professionalgeoyar22-Jan-15 16:32 
Now it was compiled and built with two warnings (I still did not look into them.)
One comment: as I remember, operator << is not overloaded for std::string. You have to use << msg.c_str() instead if you want to get characters printed.
GeneralRe: Porting to VC++ 12 Pin
Joren Heit23-Jan-15 3:10
Joren Heit23-Jan-15 3:10 
GeneralRe: Porting to VC++ 12 Pin
alexquisi23-Jan-15 5:24
alexquisi23-Jan-15 5:24 
QuestionWhich compiler? Pin
David Luca19-Jan-15 0:54
professionalDavid Luca19-Jan-15 0:54 
AnswerRe: Which compiler? Pin
Joren Heit19-Jan-15 0:57
Joren Heit19-Jan-15 0:57 
GeneralRe: Which compiler? Pin
David Luca19-Jan-15 1:01
professionalDavid Luca19-Jan-15 1:01 
GeneralRe: Which compiler? Pin
Joren Heit19-Jan-15 1:07
Joren Heit19-Jan-15 1:07 
QuestionAre there any compatibility issues? Pin
den2k8819-Jan-15 0:19
professionalden2k8819-Jan-15 0:19 
AnswerRe: Are there any compatibility issues? Pin
Joren Heit19-Jan-15 0:54
Joren Heit19-Jan-15 0:54 
GeneralRe: Are there any compatibility issues? Pin
den2k8819-Jan-15 0:58
professionalden2k8819-Jan-15 0:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.