分类: C/C++
2006-06-23 13:23:29
==============================================================================
Typesafe dynamic callbacks in C++ (signal slot system)
==============================================================================
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 . 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 :)
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
Implementationinstead 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
Implementationand inserts it to the container in Signal0
class given as first parameter. The Implementation-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-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
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, Interface1 ,
Implementation1. (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<>-------->* Interface1 *<------------------<> KObject
^ ^
| |
Implementation1----------> Receiver
(In this picture, Signal1is 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
-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:
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)
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.
Gtk toolkit implements similar system
in C language. Its implementation has problem that
signals are hard to create and it is not compiletime
typesafe.
(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
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)