Smallest possible useful C++ Attribute system?

Over the years, I've seen almost as many attribute reflection systems as I've worked on games, and that's a lot of games. This article by Gary McNickle on Gamasutra is clean and easy to read, but it is not as small as possible. It inspired me to write a few notes about how I think these things should be put together.

Here are the usage goals for the system:

  • The game code owns the variables, not the attribute system
  • The attribute system does not force the game to use it
  • Attributes can be searched, and iterated over

Out of scope:

  • Serialization to disk, and relocation
  • Implementation of features normally part of C++ or the STL

Here are some technical requirements I decided to hit:

  • Only one copy of any name of any variable would ever be stored
  • As much as possible the system will self-priming
  • The system will be typesafe
  • Methods on the class may be templated for convenient type coercion
  • Avoid "best practices" such as accessors if they don't make the code simpler

I tried several variations, and each one got shorter and less complex. I started with a template meta-programmed solution, not unlike the cleverness referenced by Gary in the Boost spirit library. The next version stripped this down to a much simpler templated attribute system; I simplified that through two rewrites. At first I pursued a template based system because the code tends to be easier to read than piles of preprocessor mess. I have a bit of a distaste for macro based systems because they tend to become unwieldy and difficult to maintain as features get wedged into the system, whereas template code remains legible. After some thought it occurred to me that if one kept the attribute system trivially simple, the macros could remain trivially simple. Templates could be used at a higher level than the base objects to implement the complexity. At this point, I overcame my built-in distaste for macro-based attribute systems, and came up with the smallest useful system I could think of.

In real life, I would not use std::string, but rather a block of data containing the char data, and indexed by offsets. Blocks of data indexed by offsets are easy to relocate to keep memory compact, and are trivial to cache of to disk. This approach also avoids the constant overhead of a string object (which tends to be 32 bytes), and also avoids any small allocations from the heap (a string takes two).

Also, I would think carefully about the use of std::map, to make sure it does not impact run time performance. A sure sign that an STL map of std::string is a poor choice is if profiling shows a measurable amount of time being spent in string comparisons.

I wonder if this can be made smaller?

 

#include <typeinfo.h> #include <string> #include <map> 
class AttributeBinder { public: class AttributeDescriptor { public: AttributeDescriptor() : ti(0), off(0) { } AttributeDescriptor(const char* name, const std::type_info& ti, ptrdiff_t off) : name(name), ti(&ti), off(off) { } AttributeDescriptor(const AttributeDescriptor& rhs) : name(rhs.name), ti(rhs.ti), off(rhs.off) { } std::string name; ptrdiff_t off; bool typeEqual(const AttributeDescriptor& rhs) { return *ti == *rhs.ti; } private: // note, must compare type infos by *m_pInfo == *rhs.m_pInfo  // because the pointers might not be the same, even if the types are  const std::type_info* ti; }; void bind(const char* varname, const std::type_info& ti, ptrdiff_t offset) { attribs[varname] = AttributeDescriptor(varname, ti, offset); } std::map<std::string, AttributeDescriptor> attribs; }; 
 // Within a class, create a binding object, and start the Bind method. // Note that it is valid to put other code between BIND_START and BIND_END // if you want it to run during the static Bind initialization. // Semicolons are optional after BIND_START, you can use them if you use // a pretty-printer on code and want things to line up nicely #define BIND_START \ static AttributeBinder binding; \ static void Bind() \ { \ // Bind a single variable #define BIND(c, v, t) \ binding.bind( #v, typeid(t), (ptrdiff_t)&((c*)0)->v) // End the bind method // Semicolons are optional after BIND_END, you can use them if you use // a pretty-printer on code and want things to line up nicely #define BIND_END \ } // Instantiate the binding object for class C, and make a globally // instantiated struct whose constructor will invoke C's Bind method // which has been declared in BIND_START // This goes in the implementation file, in the same namespace as the // class C. #define BIND_ATTRIBUTES(c) \ AttributeBinder c::binding; \ namespace { \ struct Static ## c ## Binder \ { \ Static ## c ## Binder() { c::Bind(); } \ }; \ static Static ## c ## Binder static ## c ## binder; \ } 

And here's an example of decorating a class:

 

class Foo { public: BIND_START; BIND(Foo, myInt, int); BIND(Foo, myFloat, float); BIND(Foo, bar, float); BIND_END; Foo() : myInt(1), myFloat(2), bar(3) { } int myInt; float myFloat; float bar; }; BIND_ATTRIBUTES(Foo);

 

Finally, to look up a variable, you would use your privileged knowledge of the implementation to do something like the following:

 Foo myFoo;
 // looking up myInt  std::map<std::string, AttributeBinder::AttributeDescriptor>::const_iterator i = Foo::binding.attribs.find("myInt"); // reading the int  int intVar = *(int*) (ptrdiff_t)&myFoo + i->second.off; // writing the int  *((int*) (ptrdiff_t)&myFoo + i->second.off) = 3; 

 

This can be prettied up with simple templated member functions, but I leave that as an exercise to the reader.

programming/code/rtti

Content by Nick Porcino (c) 1990-2011