Jump to content
  • Advertisement
  • 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



    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


    The system provides two main classes - Event which represents a signal that can be sent from an object, and Delegate 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.


    Lets start by looking at the Event 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. template class Event { public: void operator()(Args... args){ } } This is the basics of varadic templates. The class... Args 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: Event event; template Event { public: void operator()(int, float, const std::string&){ } }; 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 operator(): 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"); The args... syntax will be expanded in this case to 10, 23.12f, "hello", which the normal rules of function lookup will resolve to the dummy f method defined above. We could define multiple versions of f taking different parameters and the resolution would then be based on the specific parameters that Event is templated upon, as expected. Note that the names Args and args 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.


    The Event class needs to store a list of subscribers to it so that the operator() 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 Delegate, 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 Event class can deal with a representation of the subscriber templated purely on the argument list. 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...); }; 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 Args... to replace the type list, and args... to replace the parameter list, just like in the simpler Event example above. So now we can expand Event to maintain a list of AbstractDelegate pointers which will be populated by ConcreteDelegates and the system can translate a call from Event using only the argument list to call to a method of a specific type: template class Event { public: void operator()(Args... args){ for(auto i: v) i->call(args...); } private: std::vector*> v; } 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 begin() and end() iterators. Connections in this system need to be two-way in that Delegate also needs to track which Events it is connected to. This is so when the Event is destroyed, the Delegate can disconnect itself automatically. Thankfully we can use Event as-is inside AbstractDelegate since it is only templated on the argument list: template class AbstractDelegate { public: virtual void call(Args... args) = 0; private: std::vector*> v; }; 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 Delegate object that can represent any number of connections of events to member functions, so a recieving object need only contain a single Delegate instance. We need therefore to have a way to treat all AbstractDelegates as the same, regardless of their argument lists, so once again we use inheritance to accomplish this: class BaseDelegate { public: virtual ~BaseDelegate(){ } }; template class AbstractDelegate : public BaseDelegate { public: virtual void call(Args... args) = 0; }; We can now store a list of BaseDelegates inside the Delegate class that can represent any AbstractDelegate, regardless of its parameter list. We can also provide a connect() method on Delegate 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: class Delegate { public: template void connect(T *t, void(T::*f)(Args...), Event &s){ } private: std::vector v; }; For example: 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){ } }; All that really remains now is some boiler-plate code to connect Events and Delegates 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, ConcreteDelegate should only be constructable with a pointer to a reciever, a member function and an Event. Connecting an Event to an AbstractDelegate should also add the Event to the AbstractDelegate's list of Events. When an Event goes out of scope, it needs to signal all its Delegates to remove it, and when a Delegate is destroyed, it needs to tell all the Events 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 std::vector and std::remove 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 Events and Delegates and for the purposes this is designed for, it is not necessary. #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; };


    Let's look at some concrete examples of this in relation to a game project. Assume we have an Application 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 AppEvents class to pass around to initialization code to represent these and trigger these events within the Application message handler: 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); } Now when we create a game object, we just make the AppEvents instance available to its constructor: 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); } Another area this is useful is in dealing with dangling pointers to resources that have been removed elsewhere. For example, if we have a Body class that wraps a rigid body in a physics system, and a Physics 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 Body a destroyed(Body*) event that is called from its destructor. class Body { public: ~Body(){ destroyed(this); } Event destroyed; } 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 Physics 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 Physics class. 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); } In addition, any other class that holds a reference to the body that does not actually own it can choose to subscribe to the destroyed(Body*) event to nullify its own reference: class Something { public: Something(Body *ref) : ref(ref) { delegate.connect(this, &Something::refLost, ref->destroyed); } private: void refLost(Body *b){ ref = 0; } Body *ref; }; Now anywhere else in the code, you can just delete the Body instance or maintain it with a smart pointer, and it will be both removed from the Physics 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.


    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

    There are no reviews to display.

  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!