The
curiously recurring template pattern, or awesome compile time magic to make the C++ type system more dynamic.
Here is a scenario that is common enough to many C++ developers. You have just developed an event system that will send notifications to a list of registered observers. After careful analysis you have decided to use strings/enums/chars/porkchops as event identifiers. Perhaps you code resembles the below.
typedef std::string EventID;
namespace EnemyEvent
{
const EventID didSpawn = "Enemy.didSpawn";
const EventID didDie = "Enemy.didDie";
const EventID didMove = "Enemy.didMove;"
const EventID didAttack = "Enemy.didAttack"
const EventID didLevelUp= "Enemy.didLevelUp";
}
typedef std::function<void (EventID const&)> EventCallback;
Now however you ended up organizing them, event ID's are pretty simple to set up. Why? Because they are homogeneously typed. All event ID's are the same type and can be easily compared. Managing a single type in a statically typed language is trivial. After some careful consideration, you realize that an event ID alone is not sufficient data to act upon, you need some more! But what kind of data do you need exactly. It obviously depends on the type of event. However, events are not types, they are string/int/porkchops constants. They don't carry any information other than their identity. So let's make them types.
class EventBase
{
public:
~EventBase ();
virtual int intValue () const;
virtual std::string stringValue () const;
virtual std::shared_ptr<Object*> objectValue () const;
virtual Point pointValue () const;
virtual Porkchop porkchopValue () const;
vitual GodzillaEggHash . . .
};
typedef std::function<void (EventBase const *)> EventCallback;
Whew. That was crazy. For every conceivable type that might be given in every conceivable event I need to add an interface to EventBase. I don't know about you but I can't see so far into the future that I can predict every type of event that the game will generate. Lets try another approach. Why not just pass the Enemy object referred to by the event!?
typedef std::function<void (EventID const&, Enemy*)> EventCallback;
This is a nice improvement because it allows us to pass a lot of information with the event. There are a few shortfalls though. Take for instance the 'didMove' event. Where exactly did the enemy move from? I mean we can get it's current location from the enemy object, but what about it's previous location? We can add a 'previousLocation' function to the Enemy class, or we can send out a 'willMove' event before the 'didMove' event. Again we suffer from the combinatorial explosion of data and types. Not ideal.
So what then should we do? Here is a pretty damn good solution:
typedef std::function<void (EventID const&, Enemy*, void*)> EventCallback;
Gross! A void pointer is like herpes. Just don't touch it. Nevertheless, it does pretty well in basic circumstances. I mean, we have the event ID, the source and at least 32bits of additional plain old data. Still, the void pointer is a big fat warning flag that says: 'I'm going to come back and fux up your life when you present this game to an investor'. Not only that, but you can only pass PLAIN data in the void pointer, no destructors or copy constructors will be called on the faceless data.
Now I'm a picky bastard and I DEMAND that my EventCallbacks can be given any type, safely and efficiently. I might want to send a shared_ptr or a PorkchopSandwich instance to my event listeners, and I have the right, gorramit!
So I hack up a data container like so:
struct Data
{
unsigned char * bytes;
unsigned int byteCount;
};
Now when generating an event I can do the following:
Data eventData;
eventData.byteCount = sizeof(std::shared_ptr<Weapon*>);
eventData.bytes = new unsigned char[eventData.byteCount];
std::memset(reinterpret_cast<void*>(eventData.bytes), 0, eventData.byteCount);
*reinterpret_cast<std::shared_ptr<Weapon*>*>(eventData.bytes) = _enemy->weapon();
// . . .
std::for_each(_observers.begin(), _observers.end(),
std::bind(&EventCallback::operator(), _1, eventID, eventSource, eventData));
// . . .
reinterpret_cast<std::shared_ptr<Weapon*>*>(eventData.bytes)->~shared_ptr<Weapon*>();
delete [] eventData.bytes;
Holy crap that's so ugly! I don't even know if it will work, and I don't want to find out! One obvious problem is that EventCallbacks may copy the event to be handled to another thread. Then when the thread unpacks the eventData to get at the shared_ptr… KABOOOM!
So after this long meandering preamble full of contrived situations, we get to the meat of the porkchop. How to make a data type containing heterogenous safely? Templates to the rescue!
First look at the code. Then marvel at the weirdness.
class DataWrapperBase
{
unsigned char * _storage;
protected:
template<typename A>
A * rawAssign (A const& a)
{
A * init = new (_storage) A(a);
return init;
}
public:
DataWrapperBase (unsigned char * stor): _storage(stor) { }
virtual ~DataWrapperBase ( ) { }
virtual DataWrapperBase * clone (unsigned char *, unsigned char *) = 0;
};
template <typename T>
class DataWrapper : public DataWrapperBase
{
T * _ref;
public:
DataWrapper (T const& t, unsigned char * stor):
DataWrapperBase(stor), _ref(0) { _ref = this->template rawAssign<T>(t); }
~ DataWrapper ( ) { _ref->~T(); }
DataWrapperBase * clone (unsigned char * wrapmem, unsigned char * stor) {
return new (wrapmem) DataWrapper<T>(*_ref, stor);
}
};
You might think that it looks just as ugly as the code above it. Well, it's actually kind of beautiful. Notice the complete lack of casts. Here is how it works:
DataWrapperBase acts as an interface to the essential functions that provide type safety, namely destructors and copy constructors. DataWrapper acts as an implementation of the interface for each type it is instantiated for. So now we can redefine the Data type to hold arbitrary types,
safely!
struct Data
{
template<typename T>
Data (T const& t)
{
dataSize = sizeof(T);
data = new unsigned char[dataSize];
dataWrapper = new (wrapperMemory) DataWrapper<T>(t, data);
}
// . . .
Data (Data const& other)
{
data = new unsigned char[other.dataSize];
if(other.dataWrapper)
// If you follow clone you'll notice it ends up at the rawAssign
// template function. In there the copy constructor is safely
// called using the correct type.
dataWrapper = other.dataWrapper->clone(wrapperMemory, data);
}
// . . .
~Data ( )
{
if(dataWrapper)
// The destructor of DataWrapperBase is virtual, so it goes
// to the DataWrapper<T> implementation and calls T's destructor.
dataWrapper->~DataWrapperBase();
delete [] data;
}
DataWrapperBase * dataWrapper;
unsigned char * data;
unsigned int dataSize;
unsigned char wrapperMemory[sizeof(DataWrapper<int>)];
};
template<typename T>
inline T const& get_data (Data const& dat)
{
return *reinterpret_cast<T*>(dat.data);
}
In this monstrosity lies generic compile time programming, and run time type safe dynamic objects. Lets see how it works:
Data::Data<T> => DataWrapper<T>::DataWrapper<T> => rawAssign<T> => T's copy constructor.Data::Data => DataWrapperBase::clone => DataWrapper<T>::clone => DataWrapper<T>::DataWrapper<T> => rawAssign<T> => T's copy constructor.Data::~Data => DataWrapperBase::~DataWrapperBase => DataWrapper<T>::~DataWrapper<T> => T's destructor. (Technically that's not how the compiler sees it, but that's beside the point).
Data eventData(_enemy->weapon());
std::for_each(_observers.begin(), _observers.end(),
std::bind(&EventCallback::operator(), _1, eventID, eventSource, eventData));
// In the event callback function . . .
std::shared_ptr<Weapon*> weapon = get_data<std::shared_ptr<Weapon*> >(eventData);
That is it! No fiddling with memory, no worries about copying or object lifetime. It's as easy as pie! Now the above object is actually a bizarre aborted beast that you shouldn't use because it's fugly, incomplete and probably has a bug somewhere in it. I provide a safe implementation below.
You might also have noticed the bizarre way that I handled the memory for DataWrapper<T>. In that lies the seeds of an awesome optimization. If you change:
to:
unsigned char data[dataSize]; // dataSize is a const.
You might notice that you don't need any more calls to new. Hmmm.. no dynamic memory allocation, no system calls, no loops, nothing undefined. Sounds pretty sweet. In fact the above technique was used as a packaging system for a high performance realtime software synthesizer, because it is entirely non-blocking and deterministic. It was used to pass automation data to and from a realtime thread. Anyone who has worked with realtime requirements knows how crazy strict you have to be.
Here is a reasonably refined and tested version of the above, ready to use and in a single header file using only standard C++. It's called Generic. You can use it like so:
typedef ark::Generic<64> Message; // Message is 64 bytes in size.
Message msg;
msg = somePointerValue;
msg = someReferenceValue;
msg = string("this is OK");
msg = "sorry, this isn't ok"; // FAIL
msg = (void*)"I guess you can do this with static strings.";
msg = 1337;
std::ostringstream ost;
ost << msg;
msg = 0;
std::istringstream ist(ost.str());
ist >> msg;
std::cout << ark::Get<int>(msg) << std::endl; // prints 1337
Don't use streams with non-plain old data, unless you know what you're doing. And I am sure that there is some combination of templates and references that will throw up an indecipherable compiler error message. But it works very well for the most common cases.
Now going back to our original, contrived problem:
typedef ark::Generic<128> EventData;
typedef std::function<void (EventID const&, Enemy*, EventData const&)> EventCallback;
void EnemyEventOccured (EventID const& name, Enemy * enemy, EventData const& data)
{
if(name == EnemyEvent.didMove)
{
const Point currentLocation = enemy->location();
const Point previousLocation = Get<Point>(data);
// . . . do something!
}
else if(name == EnemyEvent.didAttack)
{
std::shared_ptr<Object> target = Get<std::shared_ptr<Object> >(data);
// . . . do something!
}
else // . . .
}
Tada!
As an aside, I 'invented' this technique independently when I was 19 and was super proud of myself. Then I discovered it has a name and was used by others (although not with the static memory trick, as far as I know). It's
not called 'the curiously recurring template pattern', and is the basis for much template magic. It's used all throughout the boost libraries. Speaking of boost, if you are using the boost libraries in your project then PLEASE, PLEASE, for the love of [insert deity/swearword] use boost::any instead of ark::Generic. This code has worked great for me, and is running smoothly in the wild, but boost's libraries are vetted by the best C++ programmers in the world. You should trust them more than me. That being said, there are many valid reasons to not include the boost libraries, and the Generic code has NO dependencies outside the standard C++ libraries. It's also simple enough to wrap your head around and learn something.
---
Let me end with an inappropriate rant of warning which applies to both new and experienced developers. If you have any choice in the matter, don't use C or C++. I know that many of you have an emotional or professional attachment to using the C family of languages, but hear me out.
Writing good reliable software in C++ is error prone. Errors that do occur are often obscure and difficult to debug. Writing good software in C++ often requires the use of libraries that are tied to a platform or specific vendor. Using a dynamically typed language is usually much easier and more pleasant than using a strictly typed language like C++. Finally, C/C++ once had the distinction of running almost everywhere. That era is coming to a close. On locked down mobile platforms where the vendor refuses to release tools, or on the Web, C/C++ is just not welcome anymore.
Now what are some of the valid reasons to use C++? They fall into a few distinct categories:
1) Leveraging existing libraries that are unavailable in another language.
2) Speed, speed, speed. Or, deterministic execution time (realtime).
3) Learning is fun.
Just to set the record straight, I have been developing in C++ for over 14 years and I love it, I have learned when it is appropriate and when it is overkill. No hating here, just a respect for awesome power.
Good luck!