Advice on de-uglying some code

Started by
4 comments, last by antareus 19 years ago
I'm playing with a messaging system at the moment (and yes, I know there are other options - just want to discuss this specific example). Let's say I have the following:

class Message
{
// Obvious ctor, dtor etc.
 int from_;
 int to_;
 some_enum Type_;
};

template <typename T> class DataMessage : Message
{
// ctor etc.
 T info_;
};

I've done it like this to provide an easy interface through Message pointers, and still retain the ability to add a single data element to the message. How can this be improved? As it stands, you need to dynamic_cast to get to the type-T information (because they're passed as Message pointers). Does boost::any solve this problem (removes the need for a template, but you still need to do a boost::any_cast)? Is using an enum for a type parameter the right way to get around the lack of multiple-dispatch (meaning that objects that respond to messages have to understand how they interpret specific messages). It seems like there's a lot going on here that's contra to the spirit of 'good c++ programming' - but is this a case where practicality has to take the lead? Thanks in advance, Jim.
Advertisement
The problem, of course, is the lack of C++ support for multiple dispatch. Libraries have been written to solve this problem. I've found all the ones I've looked at to be uncomfortably hackish, but your mileage may vary.

For doing stuff manually: no-throw dynamic_cast is fine. It's decently fast. Checking an enum is slightly faster, especially because the compiler can do fun things with jump tables. Unless it becomes an issue, don't bother with the enum.
I can't say I've ever gotten heavily involved in a message system so keep that in mind for the rest of this post.

You may want to try something like the following.

enum MESSAGE_TYPES_E {BASE, SPECIAL};

class Message
{
int nFrom;
int nTo;

virtual int GetType() { return BASE; }
};

class Special : public Message
{
int nExtraInfo;

int GetType() { return SPECIAL; }
};

Then you can just pass it around as usual thanks to our friend polymorphism and query its type with ease to cast it appropriately.
I know a lot of projects that use enums for type information. It adds a byte for the first 255 message types. hat's generally acceptable. If you like you can use bitfields to squeeze it down more but you lose alignment.

union pack{  uint32 u;  struct  d   {    unsigned int type : 4;    unsigned int data : 28;  }; };
Thanks for the replies.

Quote:
Libraries have been written to solve this problem. I've found all the ones I've looked at to be uncomfortably hackish, but your mileage may vary.


I have some collision stuff written using Loki's staticDispatcher (which actually works very well) - but in that particular case there are relatively few primitives to test against. The messaging system is much more complex - one of the reasons I'm playing with different options.

Quote:
You may want to try something like the following.


Yup, that's pretty much what my code does - only using a templated derived class means that you can change the information being carried (sometimes it's an int, sometimes an n-vector). Your proposal doesn't need the inheritance - all the functionality could be carried in the base class. My code needs the inheritance; if you made the base class templated, message handling classes would need to handle pointers to Message<T>, which becomes inpractical extremely quickly. Thanks for the comment though!

Quote:
I know a lot of projects that use enums for type information. It adds a byte for the first 255 message types. hat's generally acceptable. If you like you can use bitfields to squeeze it down more but you lose alignment.


Interesting to hear that projects are doing this - answers my 'practicality' question. Because I've only been learning C++ for a short while, sometimes I want to stick to the idealised philisophies (ie no type switches, do dynamic casting etc.) - now it's time for me to learn the realities of software programming. Thanks for the info on space as well - another trick worth knowing!

Thanks again,
Jim.
I like to think I've nailed this problem exactly. But keep in mind it fits my needs, and may or may not match yours. This is essentially a mutated form of signals and slots, however the way its setup makes it far more flexible.

I grabbed the FastDelegates off of CodeProject to use as my functor library (unlike Boost, it doesn't unconditionally add ten seconds to your compile time) and modified it a little, mostly just renaming it to Delegate.

My base message class I call an event. I decided that any event handler would take a single parameter - the specific event class. Using this, I can use a few generic programming techniques such as type erasure and the curiously recurring template pattern in order to essentially force the compiler to write what is fairly tedious code for us.

#ifndef OPENIM_EVENTS_EVENT_HPP#define OPENIM_EVENTS_EVENT_HPP#include "../Events.hpp"#include "../Handler.hpp"namespace OpenIM{namespace Events{	enum Priority	{		Low = 3,		Normal = 2,		High = 1	};	template<typename DerivedEvent>	struct Event	{		template<typename Class>		static void Attach(Class* instance, void (Class::* function)(DerivedEvent&), Priority priority = Normal)		{			Delegate<void (DerivedEvent&)> target(instance, function);			Handler handler;			handler.Target = target;			handler.Priority = priority;			EventAttach(static_cast<DerivedEvent*>(this)->GetName(), handler);		}		static void Attach(void (*function)(DerivedEvent&), Priority priority = Normal)		{			Delegate<void (DerivedEvent&)> target(function);			Handler handler;			handler.Target = target.GetMemento();			handler.Priority = priority;			EventAttach(DerivedEvent::GetName(), handler);		}		template<typename Class>		static void Detach(Class* instance, void (Class::* function)(DerivedEvent&))		{			Delegate<void (DerivedEvent&)> target(instance, function);			Handler handler;			handler.Target = target;			EventDetach(static_cast<DerivedEvent*>(this)->GetName(), handler);		}		static void Detach(void (*function)(DerivedEvent&))		{			Delegate<void (DerivedEvent&)> target(function);			Handler handler;			handler.Target = target;			EventDetach(static_cast<DerivedEvent*>(this)->GetName(), handler);		}		void Raise()		{			EventRaise(static_cast<DerivedEvent*>(this)->GetName(),				static_cast<DerivedEvent*>(this),				&Event<DerivedEvent>::Invoke);		}	private:		static void Invoke(void* event, const OpenIM::Handler& handler)		{			Delegate<void (DerivedEvent&)> target;			target.SetMemento(handler.Target);			target(*static_cast<DerivedEvent*>(event));		}	};}}#endif


A derived event class:
#ifndef OPENIM_EVENTS_COMPONENTEVENTS_HPP#define OPENIM_EVENTS_COMPONENTEVENTS_HPP#include "Event.hpp"#include "../Component.hpp"namespace OpenIM{namespace Events{	struct ComponentLoadEvent :		public Event<ComponentLoadEvent>	{		static const char* GetName() { return "ComponentLoadEvent"; }		OpenIM::Component*	Component;	};}}#endif


A quick discussion:
* EventAttach/EventDetach/EventRaise are openings to the system-wide handler list. Each event is keyed by name (hence the calls to the derived's static GetName() method, you may want to use an integer). The code is uninteresting, excepting the fact that it prevents infinitely recursive events.

* All of the magic happens inside of Invoke. The opaque delegate is restored to a real one (using SetMemento()) and then invoked. The base event class may be safely downcasted to its derived type - because of the curiously recurring template! The type erasure is essentially undone here.

* Event distribution happens when you call Raise() on an instance of the event:
ComponentLoadEvent e;e.Component = component;e.Raise();


* Client code can write the following:
ComponentLoadEvent::Attach(this, &MyComponent::OnComponentLoad);

void MyComponent::OnComponentLoad(ComponentLoadEvent& e){ cout << "Loaded " << e.Name << endl;}


* It sounds cocky, but I have yet to find a superior method for type-safe messaging. It is fast, works across DLL boundaries, and doesn't require RTTI.

* In the future I plan to add deferred dispatch so events can be run at a later time - this would require some sort of ownership transfer to the event system itself, along with a way to specify when the event should actually run. Marshaling is also an interesting thought, for serialization.

I should write a column with all the generic programming I've been doing lately.
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis

This topic is closed to new replies.

Advertisement