Type safe signal implementation
Hi!
I've been thinking about implementing the event handling mechanism for
my (work in progress) wrapper library for (the one and ugly) Windows
API. I've abandoned the project some time ago because of lack of free
time, but even back then I gave the subject some thought. But all I've
managed to achieve was pretty much the same as libsig and boost:signals
(of course A LOT more primitive). So when I came back to the project,
the problem returned. The decision was to use signal-slot mechanism. But
there were two aims which I felt I needed to achieve: the mechanism must
be as easy in use for client programmers as possible (even if it meant
using preprocessor macros) and yet be a "normal C++ library" (as opposed
to QT and .moc files). I've spent some time trying different
alternatives and searching for new ways of implementig signals (I even
tried things like using goto or longjmp) and after few long hours in
front of monitor I managed to implement a totally type safe (without a
single explicit cast!) signal mechanism. It's not complete - you can
only connect member functions to signals (no free functions) and only
signals with one argument and no return value are supported. I guess the
whole thing isn't probably anything new, but still I decided to post it
because of three things. First of all, I thought it wasn't even possible
to avoid casting (and I was wrong what established the position of C++
as my favourite programming language). Secondly, I'm suprised I managed
to work it out. Last, but not least - I hope that maybe someone will
find the code useful, because it includes some nice tricks. So if you're
interested, please read on and feel free to post comments.
As usual, there is no rose without thorns: the tradeoff is efficiency. I
used some STL containers for ease of implementation, but some may be
replaced - vector might be replaced by set (but one needs to provide
operator< for comparing member functions which would probably require
casts, and I promised not to use them) or fixed size array (put a limit
on number of functions that may be connected to a particular signal).
But even then I guess my implementation would be much slower than that
of boost or libsig (though I would be interested in real comparison...).
The way it works. I've discovered and learned that it's possible to put
a default-constructible variable of any type into an object at
compile-time thanks to templates and static keyword:
class MyClass
{
public:
// ...
private:
template<typename VariableT>
VariableT& getVariable()
{
static VariableT variable = VariableT();
return variable;
}
};
The "variable" is the same across all objects of MyClass, but it's easy
to avoid that using a std::map:
template<typename VariableT>
VariableT& getVariable()
{
static std::map<void*, VariableT> variables;
return variables[this];
}
That way a signal object can save pointers to members of different class
types with different argument count and types:
signal s;
s.connect(SomeClass_object, &SomeClass::someMethod);
This causes the appropriate version of template member function
"connect" to be instantiated, which saves the information about the
object and method that needs to be called. The next thing to do is to
make the signal object know, which template member functions were
instantiated and saved the information about connections. The same trick
is used. When the templatized (with argument types) operator() gets
called, it calls the appropriate execute() function, which is
additionally templatized with class type. The execute function knows the
exact type of class and arguments, so it can reach for saved
information. But execute function's argument list does not depend on
class type parameter, so pointers to all execute functions with the same
formal argument list may be put into one container and operator() gets
the information from that container. It's all a bit complicated, but I
hope the code will clarify things.
Stefan Chrobot
#include <iostream>
#include <map>
#include <vector>
using namespace std;
struct signal
{
template<typename ClassT, typename Arg1T>
struct call_info
{
call_info(ClassT* object = 0, void (ClassT::*method)(Arg1T) = 0)
: object(object), method(method)
{
}
ClassT* object;
void (ClassT::*method)(Arg1T);
};
template<typename Arg1T>
std::vector<void (signal::*)(Arg1T)>& getMethods()
{
static std::vector<void (signal::*)(Arg1T)> methods;
return methods;
}
template<typename ClassT, typename Arg1T>
std::multimap<void*, call_info<ClassT, Arg1T> >& getConnections()
{
static std::multimap<void*, call_info<ClassT, Arg1T> >
connections;
return connections;
}
template<typename ClassT, typename Arg1T>
void connect(ClassT& object,
void (ClassT::*method)(Arg1T))
{
connect_do<ClassT, Arg1T>(&object, method);
}
template<typename ClassT, typename Arg1T>
void connect_do(ClassT* object = 0,
void (ClassT::*method)(Arg1T) = 0)
{
std::multimap<void*, call_info<ClassT, Arg1T> >& connections
= getConnections<ClassT, Arg1T>();
std::vector<void (signal::*)(Arg1T)>& methods
= getMethods<Arg1T>();
if(object && method)
{
connections.insert(
std::make_pair(this,
call_info<ClassT, Arg1T>(object,
method)));
// the line below (assign pointer to variable) is a must,
// because some compilers (like G++ 3.4.2) will complain,
// that they have 'no contextual type information';
// though others (like VC8) don't need it
void (signal::*execute_method)(Arg1T)
= &signal::execute<ClassT, Arg1T>;
bool found = false;
typedef typename
std::vector<void (signal::*)(Arg1T)>::iterator iter;
for(iter it = methods.begin(); it != methods.end(); ++it)
{
if(*it == execute_method)
{
found = true;
break;
}
}
if(!found)
methods.push_back(execute_method);
}
}
template<typename ClassT, typename Arg1T>
void execute(Arg1T arg1)
{
std::multimap<void*, call_info<ClassT, Arg1T> >& connections
= getConnections<ClassT, Arg1T>();
typedef typename
std::multimap<void*,
call_info<ClassT, Arg1T> >::iterator it;
typedef std::pair<it, it> range;
range r = connections.equal_range(this);
while(r.first != r.second)
{
((*r.first).second.object->*(*r.first).second.method)(arg1);
++r.first;
}
}
template<typename Arg1T>
void operator()(Arg1T arg1)
{
std::vector<void (signal::*)(Arg1T)>& methods
= getMethods<Arg1T>();
typedef typename
std::vector<void (signal::*)(Arg1T)>::iterator iter;
for(iter it = methods.begin(); it != methods.end(); ++it)
(this->*(*it))(arg1);
}
};
struct Test
{
void print(int x)
{
cout << "Test::set " << x << endl;
}
void printMore(int x)
{
cout << "class Test, method printMore with argument x = "
<< x << endl;
}
};
struct OtherTest
{
void inc(int x)
{
cout << "OtherTest::inc " << x + 1 << endl;
}
};
int main()
{
Test t1;
Test t2;
OtherTest t3;
signal sig1;
sig1.connect(t1, &Test::print);
sig1.connect(t2, &Test::printMore);
sig1.connect(t3, &OtherTest::inc);
sig1(4);
signal sig2;
sig2.connect(t1, &Test::print);
sig2.connect(t2, &Test::print);
sig2(5); // signal with int
sig2('5'); // signal with char, nothing happens
}
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]