From: Stefan Chrobot on
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)
From: alex on
That all is good, but...

Stefan Chrobot wrote:
>
> 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)

Just a nit: any point in not overloading connect()?

[skipped]
>
> 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
> }

Right, when signalling with char nothing happens. At all. But not
only desired functions could be not called this way-- the undesired
ones could get called. Try the following:

struct YetAnotherTest
{
void dec(double x)
{
cout << "YetAnotherTest::dec " << x - 1 << endl;
// do some harm: delete user data, etc.
}
};

int main()
{
Test t1;
Test t2;
OtherTest t3;
YetAnotherTest t4;

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.connect(t4, &YetAnotherTest::dec);

sig2(5); // signal with int
sig2('5'); // signal with char, nothing happens
sig2(1.0); // YetAnotherTest::dec breaks in

return 0;
}

Such a type "safety" might become your foe.

I'd rather curious of application where this feature can be useful. As
for me, this potentially introduces problems which are hard to debug.
It would be better to have at least run-time check for a type mismatch,
but I have no idea how one could acheive such a check.

Alex


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

From: Stefan Chrobot on
{ it is useful to quote at least one line of the post to which you
follow up, even if it's your own. thanks! -mod }

Just a few words more. I found two flaws in the code.

(1) When signal object is destroyed, the information about it's
connections are not deleted (doing that is not trivial).
(2) When combined with (1), identifying object by it's address will
cause problems.

I guess this issues can be solved with some sort of custom container,
but I think it will cause even more overhead. Nevertheless, I hope
someone will find the code useful in some way.

Stefan Chrobot

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

From: Stefan Chrobot on
alex wrote:
> Just a nit: any point in not overloading connect()?
No, I just forgot to clean up the code. It's a leftover from previous
implementation.

>> sig2(5); // signal with int
>> sig2('5'); // signal with char, nothing happens
> Right, when signalling with char nothing happens. At all. But not
> only desired functions could be not called this way-- the undesired
> ones could get called. Try the following:
>
> struct YetAnotherTest
> {
> void dec(double x)
> {
> cout << "YetAnotherTest::dec " << x - 1 << endl;
> // do some harm: delete user data, etc.
> }
> };
>
> int main()
> {
> Test t1;
> Test t2;
> OtherTest t3;
> YetAnotherTest t4;
>
> 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.connect(t4, &YetAnotherTest::dec);
>
> sig2(5); // signal with int
> sig2('5'); // signal with char, nothing happens
> sig2(1.0); // YetAnotherTest::dec breaks in
>
> return 0;
> }

I'm sorry, but I don't quite follow. What do you mean by "undesired
ones"? YetAnotherTest::dec was connected to sig2 and so it was called
when the appropriate argument was passed to operator(). It's the desired
effect. Is there anything wrong with that?

Stefan Chrobot

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

From: alex on
Stefan Chrobot wrote:
>
> I'm sorry, but I don't quite follow. What do you mean by "undesired
> ones"? YetAnotherTest::dec was connected to sig2 and so it was called
> when the appropriate argument was passed to operator(). It's the desired
> effect. Is there anything wrong with that?

OK. My point is that you connect functions with different signatures
to the same signal object. And when you "raise" the signal you have to
use some particular signature. And then, only whose functions with
matching signature are called. As I said before I cannot imagine an
application there this feature will be useful (altough, I didn't said
that such an application cannot _exist_. :)

As for me, the compile-time signature check is the desired feature of a
signal system. Without such a check you may accidentally connect a
function with signature matching to some call of that signal in further
code and get the function called which is not what you want. Of
course, this is possible even if the signature of such a function is
matching the intended call... Anyway, having a compile-time check
reduces the risk in the former case.

The absence of compile-time signal signature check lets creep in bugs
which are possibly hard to detect. My point is that, having all the
risks of event-based system, your one adds some more subtle risks by
allowing signature mismatch to compile.


Alex


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]