|
From: Stefan Chrobot on 13 Sep 2006 17:18 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 14 Sep 2006 12:48 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 14 Sep 2006 12:47 { 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 14 Sep 2006 16:47 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 15 Sep 2006 10:05 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! ]
|
Pages: 1 Prev: int used in char_traits Next: Sanity check: public/private |