Commonly cited advantages of dynamic languages like Javascript or Python over C/C++ are the ability to easily mix data definitions with code in source files and to introspect classes and other code structures as if they were data. Standard C++ syntax also often requires duplicate and otherwise annoyingly verbose syntax when declaring many similar objects.
Thankfully, as with many other problems, we can overcome these limitations by creatively using the C preprocessor. Contrary to prevailing C++ doctrine, I believe that cpp macros can be the most concise, easy to understand, and high performance way to solve certain problems. With a little functional macro programming we can define reflective structs and enums with zero overhead.
Let’s say we are writing a game about building spaceships out of blocks. We have a class to represent the persistent data for each block, and we want to be able to serialize and deserialize that class, but we also read the fields of this class a gazillion times each frame and need them to be normal struct members – we can’t use a hash table or something instead. We also add or remove members frequently and don’t want to have to change the code in six places every time we do this. Here is a simple way to accomplish this.
(Note: This is simplified code from Reassembly – I hate iostreams and am not actually using them in Reassembly but for the sake of exposition it was the simplest way.)
#define SERIAL_BLOCK_FIELDS(F) \ F(uint, ident, 0) \ F(float2, offset, float2(0.f)) \ F(float, angle, 0.f) \ F(uchar, blockshape, 0) \ F(uchar, blockscale, 1) \ F(FeatureEnum, features, 0) \ ... #define SERIAL_TO_STRUCT_FIELD(TYPE, NAME, DEFAULT) \ TYPE NAME = DEFAULT; #define SERIAL_WRITE_STRUCT_MEMBER(_TYPE, NAME, DEFAULT) \ if ((NAME) != (DEFAULT)) os << #NAME << "=" << NAME << ","; struct SerialBlock { SERIAL_BLOCK_FIELDS(SERIAL_TO_STRUCT_FIELD); std::ostream& operator<<(std::ostream &os) { os << "{"; SERIAL_BLOCK_FIELDS(SERIAL_WRITE_STRUCT_MEMBER); os << "}"; return os; } ... };
That’s it. Now we can add and remove block fields without having to update the serialization routine. We can manipulate the struct fields any way we want by writing new macros and passing them into the field declaration macro. Useful examples include parsing, resetting fields to default, operator==, or listing struct fields for purposes of tab completion.
Another useful application is reflective enums/bitfields.
#define BLOCK_FEATURES(F) \ F(COMMAND, uint64(1)<<0) \ F(THRUSTER, uint64(1)<<1) \ F(GENERATOR, uint64(1)<<2) \ F(TURRET, uint64(1)<<3) \ #define SERIAL_TO_ENUM(X, V) X=V, enum FeatureEnum { BLOCK_FEATURES(SERIAL_TO_ENUM) }
At the risk of pissing off both the Effective C++ crowd and the hardcore C crowd I will introduce an elaboration using templates and the visitor design pattern for better generality. We define a function called getField(object, name) that returns the value of a reflective struct field given by name. We can use the same technique to parse/serialize arbitrary structs, list members, generate UI for editing structs, etc.
The actual game code for this is on my github repo, together with some convenient macros for defining reflective structs and enums.
#define SERIAL_VISIT_FIELD_AND(TYPE, NAME, DEFAULT) \ vis.visit(#NAME, (NAME), TYPE(DEFAULT)) && struct SerialBlock { ... template <typename V> bool accept(V& vis) { return SERIAL_BLOCK_FIELDS(SERIAL_VISIT_FIELD_AND) true; } }; template <typename T> struct GetFieldVisitor { T* value; const string field; GetFieldVisitor(const char* name) : value(), field(name) {} bool visit(const char* name, T& val, const T& def=T()) { if (name != field) return true; value = &val; return false; } template <typename U> bool visit(const char* name, U& val, const U& def=U()) { return true; } }; // get a reference to a field in OBJ named FIELD (the same as getattr in Python). // will crash if field does not exist or type is slightly wrong // use like getField(block, "angle") template <typename T> U& getField(T& obj, const char* field) { GetFieldVisitor<U> vs(field); obj.accept(vs); return *vs.value; }