• 08/17/14 12:07 PM
    Sign in to follow this  

    Using Varadic Templates for a Signals and Slots Implementation in C++

    General and Gameplay Programming

    Aardvajk

    Abstract

    Connecting object instances to each other in a type-safe manner is a well-solved problem in C++ and many good implementations of signals and slots systems exist. However, prior to the new varadic templates introduced in C++0x, accomplishing this has traditionally been complex and required some awkward repetition of code and limitations. Varadic templates allow for this system to be implemented in a far more elegant and concise manner and a signals/slots system is a good example of how the power of varadic templates can be used to simplify generic systems that were previously difficult to express. This system is limited to connecting non-static class methods between object instances in order to keep the article focused and to meet the requirements for which the code was originally designed. Connecting signals to static or non-member functions is an extension not discussed here. Events in this system also have no return type as returning values from a signal potentially connected to many slots is a non-trivial problem conceptually and would distract from the central concept here. The final product of this implementation is a single, fairly short header file that can be dropped into any C++0x project. Because I work with QtCreator, I chose to name this system as Events and Delegates rather than signals and slots as QtCreator treats certain words as reserved due to Qt's own signals/slots system. This is purely cosmetic and irrelevant to the article. A detailed discussion of varadic templates is beyond the scope of this article and will be discussed purely in relation to this specific example. Code is tested with GCC 4.7.2 but should be valid for any C++0x compliant compiler implementing varadic templates

    Overview

    The system provides two main classes - [tt]Event[/tt] which represents a signal that can be sent from an object, and [tt]Delegate[/tt] which is used to connect external signals to internal member functions. Representing the connections as members allows for modelling of the connections via the object lifetimes so that auto-disconnecting of connections when objects are destroyed can be expressed implicitly with nothing required from the programmer.

    Event

    Lets start by looking at the [tt]Event[/tt] class as an introduction to the varadic template syntax. We need a class that is templated on any number of parameters of any types to represent a generic signal sent by an object. [code] template class Event { public: void operator()(Args... args){ } } [/code] This is the basics of varadic templates. The [tt]class... Args[/tt] is expanded to a comma-separated list of the types provided when the template is instatiated. For example, under the hood, you can think of the compiler doing something like this: [code] Event event; template Event { public: void operator()(int, float, const std::string&){ } }; [/code] For simplicity, lets imagine we have a normal function taking these parameters, so we can look at how we call it from within the body of the [tt]operator()[/tt]: [code] void f(int i, float f, const std::string &s) { } template class Event { public: void operator()(Args... args){ f(args...); } } Event event; event(10, 23.12f, "hello"); [/code] The [tt]args...[/tt] syntax will be expanded in this case to [tt]10, 23.12f, "hello"[/tt], which the normal rules of function lookup will resolve to the dummy [tt]f[/tt] method defined above. We could define multiple versions of [tt]f[/tt] taking different parameters and the resolution would then be based on the specific parameters that [tt]Event[/tt] is templated upon, as expected. Note that the names [tt]Args[/tt] and [tt]args[/tt] are arbitrary like a normal template name. The ellipses is the actual new syntax introduced in C++0x. So we now have a class to represent an event that can be templated on any combination and number of parameters and we can see how to translate that into a function call to a function with the appropriate signature.

    Delegate

    The [tt]Event[/tt] class needs to store a list of subscribers to it so that the [tt]operator()[/tt] can be replaced by a method that walks this list and calls the appropriate member of each subscriber. This is where things become slightly more complicated because the subscriber, a [tt]Delegate[/tt], needs to be templated both on its argument list and also the type of the subscriber object itself. Core to the whole concept of generic signals and slots is that the signal does not need to know the types of the subscriber objects directly, which is what makes the system so flexible. So we need to use inheritance as a way to abstract out the subscriber type so that the [tt]Event[/tt] class can deal with a representation of the subscriber templated purely on the argument list. [code] template class AbstractDelegate { public: virtual ~AbstractDelegate(){ } virtual void call(Args... args) = 0; }; template ConcreteDelegate : public AbstractDelegate { public: virtual void call(Args... args){ (t->*f)(args...); } T *t; void(T::*f)(Args...); }; [/code] Note that the varadic template usage is just being combined with the existing pointer-to-member syntax here and nothing new in terms of varadic templates is introduced. Again we are simply using [tt]Args...[/tt] to replace the type list, and [tt]args...[/tt] to replace the parameter list, just like in the simpler [tt]Event[/tt] example above. So now we can expand [tt]Event[/tt] to maintain a list of [tt]AbstractDelegate[/tt] pointers which will be populated by [tt]ConcreteDelegates[/tt] and the system can translate a call from [tt]Event[/tt] using only the argument list to call to a method of a specific type: [code] template class Event { public: void operator()(Args... args){ for(auto i: v) i->call(args...); } private: std::vector*> v; } [/code] Note the use of the for-each loop also introduced in C++0x. This is purely for brevity and not important to the article. If it is unfamiliar, it is just a concise way to express looping across a container that supports [tt]begin()[/tt] and [tt]end()[/tt] iterators. Connections in this system need to be two-way in that [tt]Delegate[/tt] also needs to track which [tt]Events[/tt] it is connected to. This is so when the [tt]Event[/tt] is destroyed, the [tt]Delegate[/tt] can disconnect itself automatically. Thankfully we can use [tt]Event[/tt] as-is inside [tt]AbstractDelegate[/tt] since it is only templated on the argument list: [code] template class AbstractDelegate { public: virtual void call(Args... args) = 0; private: std::vector*> v; }; [/code] The final class that we need to look at is motivated by the fact that creating a separate object inside each recieving class to represent each slot is tedious and repetitive, since the recieving object requires both a member function to be called in response to the signal, then an object to represent the connection. The system instead provides a single [tt]Delegate[/tt] object that can represent any number of connections of events to member functions, so a recieving object need only contain a single [tt]Delegate[/tt] instance. We need therefore to have a way to treat all [tt]AbstractDelegates[/tt] as the same, regardless of their argument lists, so once again we use inheritance to accomplish this: [code] class BaseDelegate { public: virtual ~BaseDelegate(){ } }; template class AbstractDelegate : public BaseDelegate { public: virtual void call(Args... args) = 0; }; [/code] We can now store a list of [tt]BaseDelegates[/tt] inside the [tt]Delegate[/tt] class that can represent any [tt]AbstractDelegate[/tt], regardless of its parameter list. We can also provide a [tt]connect()[/tt] method on [tt]Delegate[/tt] to add a new connection, which has the added advantage that the template arguments can then be deduced by the compiler at the point of call, saving us from having to use any specific template types when we actually use this: [code] class Delegate { public: template void connect(T *t, void(T::*f)(Args...), Event &s){ } private: std::vector v; }; [/code] For example: [code] class A { public: Event event; }; class B { public: B(A *a){ delegate.connect(this, &B::member, a->event); } private: Delegate delegate; void member(int i, float f){ } }; [/code] All that really remains now is some boiler-plate code to connect [tt]Events[/tt] and [tt]Delegates[/tt] and to auto-disconnect them when either side is destroyed. A detailed discussion of this is not really related to varadic templates and just requires some familiarity with using the standard library methods. Fundamentally, [tt]ConcreteDelegate[/tt] should only be constructable with a pointer to a reciever, a member function and an [tt]Event[/tt]. Connecting an [tt]Event[/tt] to an [tt]AbstractDelegate[/tt] should also add the [tt]Event[/tt] to the [tt]AbstractDelegate's[/tt] list of Events. When an [tt]Event[/tt] goes out of scope, it needs to signal all its [tt]Delegates[/tt] to remove it, and when a [tt]Delegate[/tt] is destroyed, it needs to tell all the [tt]Events[/tt] it is listening to to remove it. Explcit disconnection is not implemented here but could be trivially added if required. An implementation of this full system just uses the usual [tt]std::vector[/tt] and [tt]std::remove[/tt] methods of the standard library. Note in this implementation, all classes are defined to be non-copyable as it is hard to come up with a sensible strategy for copying behaviour of both [tt]Events[/tt] and [tt]Delegates[/tt] and for the purposes this is designed for, it is not necessary. [code] #include #include template class Event; class BaseDelegate { public: virtual ~BaseDelegate(){ } }; template class AbstractDelegate : public BaseDelegate { protected: virtual ~AbstractDelegate(); friend class Event; void add(Event *s){ v.push_back(s); } void remove(Event *s){ v.erase(std::remove(v.begin(), v.end(), s), v.end()); } virtual void call(Args... args) = 0; std::vector*> v; }; template class ConcreteDelegate : public AbstractDelegate { public: ConcreteDelegate(T *t, void(T::*f)(Args...), Event &s); private: ConcreteDelegate(const ConcreteDelegate&); void operator=(const ConcreteDelegate&); friend class Event; virtual void call(Args... args){ (t->*f)(args...); } T *t; void(T::*f)(Args...); }; template class Event { public: Event(){ } ~Event(){ for(auto i: v) i->remove(this); } void connect(AbstractDelegate &s){ v.push_back(&s); s.add(this); } void disconnect(AbstractDelegate &s){ v.erase(std::remove(v.begin(), v.end(), &s), v.end()); } void operator()(Args... args){ for(auto i: v) i->call(args...); } private: Event(const Event&); void operator=(const Event&); std::vector*> v; }; template AbstractDelegate::~AbstractDelegate() { for(auto i : v) i->disconnect(*this); } template ConcreteDelegate::ConcreteDelegate(T *t, void(T::*f)(Args...), Event &s) : t(t), f(f) { s.connect(*this); } class Delegate { public: Delegate(){ } ~Delegate(){ for(auto i: v) delete i; } template void connect(T *t, void(T::*f)(Args...), Event &s){ v.push_back(new ConcreteDelegate(t, f, s)); } private: Delegate(const Delegate&); void operator=(const Delegate&); std::vector v; }; [/code]

    Examples

    Let's look at some concrete examples of this in relation to a game project. Assume we have an [tt]Application[/tt] class that is called when a Windows message is processed. We want to be able to have game objects subscribe to certain events, such as key down, application activated etc. So we can create an [tt]AppEvents[/tt] class to pass around to initialization code to represent these and trigger these events within the [tt]Application[/tt] message handler: [code] class AppEvents { Event activated; Event keyDown; }; class Application { public: LRESULT wndProc(UINT msg, WPARAM wParam, LPARAM lParam); private: AppEvents events; }; LRESULT Application::wndProc(UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_ACTIVATE: events.activated(static_cast(wParam)); return 0; case WM_KEYDOWN : if(!(lParam & 0x40000000)) events.keyDown(wParam); return 0; case WM_LBUTTONDOWN: events.mouseDown(Vec2(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)), VK_LBUTTON); return 0; } return DefWindowProc(hw, msg, wParam, lParam); } [/code] Now when we create a game object, we just make the [tt]AppEvents[/tt] instance available to its constructor: [code] class Player : public GameItem { public: Player(AppEvents &events, const Vec3 &pos); private: void appActivated(bool state){ /* ... */ } void keyDown(int key){ /* ... */ } Delegate delegate; }; Player::Player(AppEvents &events, const Vec3 &pos) : pos(pos) { delegate.connect(this, &Player::appActivated, events.activated); delegate.connect(this, &Player::keyDown, events.keyDown); } Player *Application::createPlayer(const Vec3 &pos) { return new Player(events, pos); } [/code] Another area this is useful is in dealing with dangling pointers to resources that have been removed elsewhere. For example, if we have a [tt]Body[/tt] class that wraps a rigid body in a physics system, and a [tt]Physics[/tt] class that is responsible for adding and removing bodies to the world, we may end up with references to a body that need to be nullified when the body is removed. It can be useful then to give the [tt]Body[/tt] a [tt]destroyed(Body*)[/tt] event that is called from its destructor. [code] class Body { public: ~Body(){ destroyed(this); } Event destroyed; } [/code] The physics system can then connect to this event when it creates the body and use it to remove the body from the physics world when it is destroyed. This saves having each body storing a reference to the [tt]Physics[/tt] instance and manually calling it from its destructor and means the body removal no longer needs to be part of the public interface of the [tt]Physics[/tt] class. [code] Body *Physics::createBody() { pRigidBody *b = world->createBody(); Body *body = new Body(); body->setRigidBody(b); delegate.connect(this, &Physics::bodyDestroyed, body->destroyed); return body; } void Physics::bodyDestroyed(Body *body) { pRigidBody *b = body->getRigidBody(); world->removeBody(b); } [/code] In addition, any other class that holds a reference to the body that does not actually own it can choose to subscribe to the [tt]destroyed(Body*)[/tt] event to nullify its own reference: [code] class Something { public: Something(Body *ref) : ref(ref) { delegate.connect(this, &Something::refLost, ref->destroyed); } private: void refLost(Body *b){ ref = 0; } Body *ref; }; [/code] Now anywhere else in the code, you can just delete the [tt]Body[/tt] instance or maintain it with a smart pointer, and it will be both removed from the [tt]Physics[/tt] world and also any other non-owning references to it get the opportunity to be updated, without the overhead of having to call methods on every possible object that might own such a reference.

    Conclusion

    Varadic templates are a powerful addition to C++ that make code that was previously verbose and limited far more elegant and flexible. This is only one example of how they allow for systems that have both type-safety and generic features implemented at compile time. The days of dreading the ellipse operator are over, since we can now use it in a type-safe manner and the possiblities are endless.


      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now


    Henderson

    Report ·

      

    Share this review


    Link to review
    grs

    Report ·

      

    Share this review


    Link to review
    Kwizatz

    Report ·

      

    Share this review


    Link to review
    Orymus3

    Report ·

      

    Share this review


    Link to review
    iedoc

    Report ·

      

    Share this review


    Link to review
    jwezorek

    Report ·

      

    Share this review


    Link to review
    tookie

    Report ·

      

    Share this review


    Link to review
    Sponji

    Report ·

      

    Share this review


    Link to review
    rave3d

    Report ·

      

    Share this review


    Link to review
    AndiDog

    Report ·

      

    Share this review


    Link to review
    Navyman

    Report ·

      

    Share this review


    Link to review
    Juliean

    Report ·

      

    Share this review


    Link to review
    Alkan

    Report ·

      

    Share this review


    Link to review
    Sirus6b

    Report ·

      

    Share this review


    Link to review
    Laval B

    Report ·

      

    Share this review


    Link to review
    Rattrap

    Report ·

      

    Share this review


    Link to review
    Dave Hunt

    Report ·

      

    Share this review


    Link to review
    Migi0027

    Report ·

      

    Share this review


    Link to review