So first off, hello to everybody, sorry for having been away for so long. Life can get pretty busy sometimes.
EDIT 1: TMP stands for "Template Meta-programming." It's a newer technique for writing C++ code that generates C++ code via the standard compiler.
EDIT 2: This post is intended for Glest programmers (sorry, no modding info here)
I started this out as an email to the list, but I figured that, with the size of it, this is probably a better place for it.
I'm still playing with a new mechanism here, and it's pretty cool. However, it's a fair distance from being ready. None the less, I want to show it to you guys! This is a first run actually implementing these techniques and it'll get cleaner as I learn this stuff better and implement it into pretty macros. So don't let the ugliness scare you off too quickly! Since this is a big posting, I'll separate it into sections.
[big]Summary[/big]
In a nutshell, we add some code to util.h (or some such) then a little at the top of network_mesage.h and, finally, we change the member definition of the message classes as follows:
class MyMessage : public Message {
private:
DEFINE_MSG_MEMBERS(
NetworkString<16>, someString,
int, someInt,
float, someFloat
)
public:
// constructor, destructor, etc.
virtual void read(NetworkDataBuffer &buf);
virtual void write(NetworkDataBuffer &buf) const;
};
Then, in NetworkMessage.cpp, we define the read and write functions as follows:
void MyMessage::read(NetworkDataBuffer &buf) {
metafunc_it::read(buf, this);
}
void MyMessage::write(NetworkDataBuffer &buf) const {
metafunc_it::write(buf, this);
}
All of the labor of sorting out types and such is performed at compile time. No actual function call is made here when compiled with optimizations. Instead, all of the code to do the read & write is completely unrolled into the function bodies, and there's no looping. The only possible exception is if the compiler decides that something in NetworkDataBuffer or it's base class should be out-lined.
[big]Gory Details[/big]
For the gory details, this is what we add to util.h (or some such). This is the code for iterating through a "static list" (i.e., a compile-time list) and performing a generic read/write operation. This can likely be cleaned up with boost:bind or Boost.Fusion, but I haven't gotten that far yet.
#include <boost/mpl/list.hpp>
#include <boost/mpl/deref.hpp>
#include <boost/mpl/next.hpp>
#include <boost/mpl/begin_end.hpp>
// This is a recursive template. Notice that it calls it's self in each function.
template<typename S, typename C, typename I = typename begin<S>::type> struct list_it {
static void read(NetworkDataBuffer &buf, C &msg) {
boost::mpl::deref<I>::type::apply(buf, msg);
list_it<S, C, typename boost::mpl::next<I>::type >::read(buf, msg);
}
static void write(NetworkDataBuffer &buf, const C &msg) {
boost::mpl::deref<I>::type::apply(buf, msg);
list_it<S, C, typename boost::mpl::next<I>::type >::write(buf, msg);
}
};
// This template specialization is what's called a "recursion terminator", as it prevents
// the above template from becoming infinitely recursive.
template<typename S, typename C> struct list_it<S, C, typename end<S>::type > {
static void read(C &o) {}
static void write(C &o) {}
};
Next, this is defined at the top of network_message.h
// This is the actual metafunction collection passed later on to list_it. Here, I'm storing
// the pointer to the data member at compile time (when the template is instantiated), a
// feature introduced in C++98
template<typename R, typename C, R C::* M>
struct net_member_funcs {
static void read(NetworkDataBuffer &buf, C &msg) {buf.read(msg.*M);}
static void write(NetworkDataBuffer &buf, const C &msg) {buf.write(msg.*M);}
};
Finally, this is what the Message-derived classes look like after pre-processing. I'm using recursive vardic pre-processor macros, a new trick I've recently learned.
class MyMessage : public Message {
private:
NetworkString<16> someString;
int someInt;
float someFloat;
typedef boost::mpl::list<
net_member_funcs<NetworkString<16>, MyMessage, &MyMessage::someString>,
net_member_funcs<int, MyMessage, &MyMessage::someInt>,
net_member_funcs<float, MyMessage, &MyMessage::someFloat>
> metafuncs;
typedef list_it<metafuncs, A> metafunc_it;
virtual void read(NetworkDataBuffer &buf);
virtual void write(NetworkDataBuffer &buf) const;
};
This constructs, at compile time, a list (that is actually just a template that recurses 3 times, plus a terminator) which stores each of my data members. Passing the types of the data members shouldn't be necessary, because I can have the template figure that out automatically, I just haven't gotten the syntax right yet.
So then, when I actually have an instance of MyMessage and I'm ready to read or write, I just have to call metafunc_it::read(buf, this); or metafunc_it::write(buf, this); and it all happens by magic.
[big]More Than Just Network Data[/big]
So I'll get back with you guys on this when it's much more solid. I also want to include features to read & write XML (not needed so much for network messages) and also to dump the contents of each message in the pretty formatted fashion I have now, which looks something like this:
MyMessage = {
Message = {
type = 1
simRxTime = 5793459823
}
someString = "hello world"
someInt = 42
someFloat = 3.14159265
}
I'm doing "intrusive" so the classes have to be modified. Boost.Serialization lets you do all of this unintrusively, which means you don't have to change the classes. However, a.) it's a huge bloated fat monster weighing in at 400kb!!!, b.) it uses RTTI and c.) it is much more CPU expensive.
So all that would have to be added is a function (like below) and the functionality added to the member_funcs struct. In practice, however, it will eventually be separate metafunctions cleverly glued together at compile time using techniques I have yet to learn, as having multiple metafunctions in a single struct is considered a "Blob" in TMP, which is an anti-pattern as it slows down compile time unnecessarily. So the implementation of these is just the addition of a few functions to the class:
class MyClass {
...
virtual void dump(ObjectPrinter &op) const;
virtual void read(const XmlNode &node);
virtual void write(XmlNode &node) const;
};
// in cpp file:
void MyClass::dump(ObjectPrinter &op) const {
BaseClass::dump(op); // force call to base class, which should do the same thing this one does.
metafunc_it::dump(op, this);
}
void MyClass::read(const XmlNode &node) {
metafunc_it::read(node, this);
}
void MyClass::write(XmlNode &node) const {
metafunc_it::write(node, this);
}
Further, all of these function definitions could be encapsulated in pre-processor macros if it made sense to do so. This is inconvenient at times, especially when debugging or attempting to decypher code you haven't seen before.
All of this is actually the basis of the data engine of a new (and quite ambitious) project I plan on launching next year to better facilitate open source gaming in general.
EDIT: fixed formatting and added section I forgot.