Static reflection in C++ using minimal repetition

jwatte's picture

Don't Repeat Yourself.

That's a great rule for writing code. If you find that you repeat yourself in code, then you're probably doing something wrong. Writing code should be about expressing what's unique about something, not filling out standard forms of data. (Copy-and-Paste coding is the worst version of this)

Unfortunately, when it comes to providing serialization and editing information about classes in C++, the language falls down. In C#, and other .NET languages, and even in Java, reflection is rich and allows you to build nice, automated editors and serializers using minimal mark-up. Also, if you need mark-up, that mark-up can be done in-place where members are defined, typically using custom attributes.

The C++ compiler actually builds all the information needed to do reflection internally. However, while the language designers provided a type_info object to represent the runtime type of a class, they stopped short of actually making that type useful. The only thing you can use a type_info for is to find the name of a type as a string, and to test equality of two types. Would it have been so hard to define some set of structures/tables to describe the layout of an object, one wonders? (multiple virtual inheritance, pointer-to-member-function members, and other such features would complicate the necessary structures, but only marginally -- it's totally doable in a portable way)

So, everyone who works on networking code or high-performance or compact file loading/saving end up writing their own metadata package. Typically, you declare the structure and the members of the structure in some set of macros. Often, those macros have to live in some C++ file that is separate from the header that declares the structure, which leads to version mismatch problems -- add a field in a struct, forget to add it to the C++, spend hours looking for a bug. Othertimes, the mark-up is too verbose and complex to get it right, leading to sometimes subtle bugs being introduced, and hard to track down, because everything is as set of macros, and you can't easily see what the compiler is doing behind your back.

So, here's a package I developed to do C++ metadata for serialization. It can be expanded to support things like STL strings, pointer-to-types, etc if you wish; perhaps I will return with an updated version later.

You declare your struct types like so:

struct foo {
  int x, y, z;
  RTTI(foo, MEMBER(x) MEMBER(y) MEMBER(z))
};

And you can use them like so:

int main() {
  printf("type: %s\n", foo::info().name());
  printf("size: %ld\n", foo::info().size());
  for (size_t i = 0; i != foo::info().memberCount(); ++i) {
    printf("  %s: offset %ld  size %ld  type %s\n",
      foo::info().members()[i].name, 
      foo::info().members()[i].offset,
      foo::info().members()[i].type->size(), 
      foo::info().members()[i].type->name());
  }
  return 0;
}

Clearly, the strength of this approach comes when you use template functions, or functions taking type information, within the implementation of your code. For example, you can write a generic "marshal" and "demarshal" function that uses the information about each struct to propery pack and unpack to a stream. The current implementation just uses binary copy, but you can easily add support for specific types by using template specialization of the Type template type:

template<typename T> struct Type : TypeBase
{
  static Type<T> instance;
  // custom marshalling is handled by template specialization
  void Marshal(void *dst, void const *src) const { memcpy(dst, src, sizeof(T)); }
  void Demarshal(void const *src, void *dst) const { memcpy(dst, src, sizeof(T)); }
  char const *name() const { return typeid(T).name(); }
  size_t size() const { return sizeof(T); }
};

Note that this is for the base member types -- int, float and such -- you don't need to write this for the higher-level structures that you use RTTI for. You can also use type tags for the structs to provide recursive marshalling of structs-as-members-in-structs, but I'm leaving it at this for now.

The rest of the code, in a form that compiles and runs using gcc 4.2.4, can be downloaded in the archive attached to this post. Please let me know what you think of it in the comments section.

Happy hacking!

AttachmentSize
reflect.zip1.38 KB