C-Scene Issue #3
Typesafe dynamic callbacks in C++ (signal slot system)
Tero Pulkkinen

This article is intended for advanced C++ programmers who want to
learn how to utilize C++'s static type system and object model
better. If you have problems understanding it, read it again. If you
still have problems ask a question from me via mail or ask on IRC from
the #c channel.

Reducing coupling between program modules is very important to be able
to reuse the modules. Still objects that are independent needs to
communicate with each other to get the job done. This article tries
to show a method for making independent objects communicate without
causing dependencies between the two objects.

Normal communication mechanism provided by the C++ language looks like this:

   Class1 <>------------>* Class2

Here Class1 has a pointer(s) to objects of class2 and object of Class1 can
call member functions of Class2 through that pointer.

In the middle, I'll explain some terminology used in this article and in the ASCII art pictures, if you know things like these already, you can skip this section. If you don't know this already, this section will be very useful for you for more than just reading this article (it uses somewhat standard notation used for drawing pictures about class hierarchies). I'll explain some of the terminology used in the ASCII art pictures: Class1 <>------------> Class2 This means that Class1 is responsible of one object of class2, this means, that when object of Class1 is destroyed, the object of Class2 is also destroyed. This can be implemented by either having variable of type Class2 inside Class1 or having pointer to object of class2 inside Class1 and having destructor of Class1 to destroy the object of Class2. Class1 knows of the existence of the object of Class2 and can use its services. This is also called aggregation. Class1 <>---------->* Class2 This means exactly same than above, cept that you can have 0...n objects as aggregates inside Class1. The extra black dot(*) is there for showing that you can have more than one object in it. This can be implemented by a dynamic container, like a linked list keeping objects of Class2 or pointers of class2 and destroying the objects at destructor of Class1. Class1 -------------> Class2 This means a reference of Class2 inside Class1. But it is only a reference, and thus it should be implemented as a pointer to one object of class2. Class1 is not responsible of that object of Class2, but only uses its services. Class1 ------------>* Class2 This is multiple references of Class2 inside Class1. This is also implemented as dynamic container inside Class1 keeping pointers of Class2. Class1 ^ | Class2 This means inheritance between two classes. Class2 derives from a base class Class1. Class1 - - - - - - > Class2 This means Class1 is used to create objects of Class2. (not used anywhere in this article :)
Ok, tutorial of concepts is over... The main problem in communication between two C++ objects is that you need to have a reference or a pointer to the destination object to be able to call one of its functions. Also, by specifying exactly the type of the object while defining the pointer you fix the interface you use to communicate between those objects. All these things cause that you have tight coupling between the two objects -- you need to know exactly who you're communicating with when you're implementing the object! The method presented in this article lets you defer the decision of who you want to communicate with -- actually it lets you even change it in runtime without the restrictions current C++'s builtin system imposes.
The Design Patterns -book instructs us to use the Bridge Pattern to reduce coupling between the objects and to make it possible to change two objects independently so that effects of changes don't spread all over the code. The following picture explains the structure of bridge pattern: Class1 <>------------------->* Interface ^ | Class2 (Those who have read Design Patterns -book notices that I left one class out from the bridge -- but it does not matter much here -- Actually in the final implementation the 4th class is there, but Class1 is used as data members inside the missing class, instead of deriving it from Class1 like in gofbook... In C++ a data member and a base class behaves pretty much the same way) Note that bridge pattern uses dynamic binding between Interface and Class2. Thus there can be more than one different implementation of Class2 (all those implementations must implement everything specified at Interface-class) and it can be changed in runtime which implementation of Class2 is used. The problem here is that class2 needs to know about Interface. Inheritance causes tight coupling between the classes and thats what we're trying to avoid -- Actually we don't want to have Class1 or Class2 have tight coupling with anything that specifies too tightly who it can communicate with. Since interface-class fixes the signature of the function to be called, we don't want that Class2 must depend from it. The dependency between Interface and Class2 can be solved by delegation: Class1 <>--------------------->* Interface ^ | Implementation -----------------> Class2 Now we have two independent classes and in the middle a communication system. To send a message, Class1 needs to know it is part of the communication system, but it does not need to know who are the receivers of the message -- there can be many receivers for same message.
We can now inspect more carefully the abilities of this design. Class1 has a dynamic container (a linked list or something) consisting of items that conform Interface. Implementation of the interface knows how to communicate with Class2 -- Note that you can for example change name of the function to be called, add or remove parameters, change order of parameter etc...(this is all done in the delegation) But this flexibility has a price to pay. For each different functions Implementation calls from Class1 you must have one extra function that redirects the call specified by Interface and implemented by Implementation to some function in Class2. This would not be a problem, but this is almost every time exactly the same, just the function names change. If users need to manually type it every time they want to use the communication system, they'll rather live with the deficiencies of tight coupling -- and lose possibility to reuse the objects they have made. But happily everything between Class1 and Class2 can be generated by C++'s classes and its parametric polymorphism mechanism and thus it can be transparent to the user of the messaging system. Implementation-class needs to know Class2's type to call its functions. We'll emphasize this dependency by using notation Implementation<Class2> instead of plain Implementation. (this notation isn't chosen randomly, it is exactly same notation C++ uses for parametric polymorphism a.k.a templates)
Let's think of an example. Let's say you have a GUI application where you want to make a dialog box where you have clear-button which clears all fields in the dialog box. We have independently implemented classes KButton and KRadioButton. Now we're trying to do KDialogbox which has for example two radiobuttons and the clear-button. How are we going to connect clear button to the other objects so that we don't cause coupling between KButton and KRadioButton? The KRadioButton class obviously have method called clear() which can be used to clear that object. KButton will be able to send an event when it is pressed. How can we connect the buttonpress event to clear() methods of both radiobuttons in our dialog box? The solution uses the following syntax in C++: class KButton { public: Signal0 buttonPressed; // this object can send a buttonPressed() -event private: void foo() { buttonPressed(); } // here we send a buttonPressed()-signal }; class KRadioButton { public: void clear() { ... } // resets state of this radiobutton to default value }; class KDialog { KButton clearbutton; KRadioButton radiobutton1,radiobutton2; public: KDialog() { // here we dynamically connect clearbutton's buttonPressed-signal // to radiobutton1's clear() -method. I.e. when clearbutton's // buttonPressed() is called, the "system" calls radiobutton1.clear() // and radiobutton2.clear() automatically connect(clearbutton.buttonPressed,radiobutton1,KRadioButton::clear); // same is done for radiobutton2 connect(clearbutton.buttonPressed,radiobutton2,KRadioButton::clear); } // the following just emulates pressing a button. void test() { clearbutton.buttonPressed(); } }; void main() { KDialog d; d.test(); } In this example KButton and KRadioButton are completely independent of each other. KDialog knows both KButton and KRadioButton and knows how they need to interact and does form connection between them. The important parts of the system we havent mentioned is the nature of Signal0 -class. It is a function object (i.e. it can be called) and it delegates the call to an abstract interface (compare that to Interface class). The Signal0 class includes a container of objects derived from Interface. The connect() function creates a new object of class Implementation<Class2> and inserts it to the container in Signal0 class given as first parameter. The Implementation<Class2> -object has all the information needed for calling the method in Class2 (== RadioButton1::clear() ). This way, when you call a KButton::buttonpressed(), the implementation of Signal0, will go through all inserted Implementation<Class2>-objects and calls a method in them, which again delegates the call to the receiving object.
Because making a connection allocates some memory, we need to have way to disconnect the connections. This can be made automatic by requiring that the receiving object is derived from certain object (let's call this KObject), and information about the connection is placed also to a container in this KObject(*). (Note, This also causes tight coupling between Class2 and KObject -- but in this case it isn't that important because KObject only handles destruction of connections and it doesn't restrict with who the Class2 can communicate) The important thing here is that the KObject does not depend on signal's signature. (*) A sidenote, there are people who dislike having to derive everything from one base class. This would indeed be a problem, if C++ didn't have multiple inheritance of implementation (Java doesn't have), or if the class would implement things that are not needed on every class. We could get rid of this requirement, if we drop safety of the system and force users to handle disconnecting themselves every time. But there's no reason for that, C++ gives pretty powerful tools for implementing these things. Signal0 <>-------->* Interface0 *<------------------<> KObject ^ ^ | | Implementation0<Receiver> -------------> Receiver Important thing to note from that picture is that Interface0 has two objects responsible from it -- if either Signal0 or KObject is destroyed, the connection is also disconnected. This way a destructor of the receiving object(Receiver) can disconnect the connections. Of course, the signal0's destructor also disconnects the connections. (This sounds odd, now we have two independent positions which can do the disconnection -- we need to have this since we need to disconnect the connections if either the receiver or the sender is destroyed). Manual disconnection by user can also be implemented by returning an object with a reference to the user.
Then there's still a problem of how to pass arguments through the signal system. For that, C++'s templates are used to be able to give any type of arguments for a signal. This is why we named Signal0 the way we did. The number (0) in the name says it does not take any parameters. We need separate implementations for 1,2,3,4... parameters. For one parameter, we would have types Signal1<Param1Type>, Interface1<Param1Type>, Implementation1<Receiver,Param1Type>. (For making it shorter, Param1type is now called P1) The complete implementation of our callback system is in form (this only applies to system with one parameter): Signal1<P1> <>-------->* Interface1<P1> *<------------------<> KObject ^ ^ | | Implementation1<Receiver,P1> ----------> Receiver (In this picture, Signal1<P1> is a function object that can be placed anywhere in user code and Receiver is also user's object - The rest is part of the messaging system and is generated automatically when Signal1<P1> -object and connect() -function are used.) Now we only have one problem -- how to generate implementations of our messaging system for 1...5 parameters. C++ is not powerful enough to generate it automatically for us, so we need to use preprocessor and ugly #defines to generate the actual messaging system(== cut/paste the implementation). For more information on how to do this - look into the web page found in references section. This however does not have negative impact on maintainability of any code cept the messaging system itself - and the system does improve maintainability of user code very much! Thank god the messaging system does not need to be changed once it is implemented. Summary: * Communication between two objects in C++ requires that the sender knows exactly the structure of receiver to be able to call one of its functions => This dependency you need to create limits reuse of the sender => the sender's implementation gets filled with things that does not belong to that object * Sollution is to create a messaging system(or signal/slot system) which makes the two objects completely independent of each other but allows 3rd object to create connections in runtime between the two objects * This can be implemented with C++ without losing advantages of compile time (static) typechecking References: www.iki.fi/terop/sigslot.html Our implementation of signal/slot system, complete implementation can be got from there. Something like this implementation is now used in gtk-toolkit's C++ interface. (names of classes are different from class names in this article, but it shouldn't be hard to figure out whats happening in there) www.troll.no Qt GUI library uses similar signal/slot system, but it requires use of extra preprocessor to implement the same thing - everyone using the library needs that preprocessor and needs to know how it works to use it. www.cs.umn.edu/~amundson/gtk/ Gtk toolkit implements similar system in C language. Its implementation has problem that signals are hard to create and it is not compiletime typesafe. www.cs.tut.fi/~jaaksi/omtpp/omtpp.htm (from that page look the paper "Typesafe callbacks with abstract partners") This page uses different approach to solve the same problem. That approach requires that users manually implement "abstract partner"-classes. "Design Patterns, Elements of reusable Object-Oriented Software", Gamma, Helm, Vlissides, Johnson 1995, Addison Wesley ISBN 0-201-63361-2 www.maths.warwick.ac.uk/c++/pub/wp/html/cd2/ C++ public review Document (The draft standard of C++) (DISCLAIMER: we did not invent anything described in this document, we just made an implementation with C++ and now documented what we actually did while implementing it)

This page is Copyright © 1997 By C Scene. All Rights Reserved