"Reflect" is in my opinion clearly the best way to do member-enumeration in C++. And yet almost nobody uses it. A quick reminder :
the reflection visitor pattern is that every class provides a member function named Reflect which takes a templated functor
visitor and applies that visitor to all its members; something like :
with Reflect you can efficiently generate text IO, binary IO, tweak variable GUIs, etc.
void Reflect( functor visit )
// (for all members)
(actually instead of directly calling "visit" you probably want to use a macro like #define VISIT(x) visit(x,#x))
A typical visitor is something like a "ReadFromText" functor. You specialize ReadFromText for the basic types (int, float), and for
any type that doesn't have a specialization, you assume it's a class and call Reflect on it. That is, the fallback specialization for
every visitor should be :
void operator () ( visiting & v )
v.Reflect( *this );
The standard alternative is to use some macros to mark up your variables and create a walkable set of extra data on the side. That is much worse in many ways, I contend. You have to maintain a whole type ID system, you have to have virtuals for each type of class IO (note that the Reflect pattern uses no virtuals). The Reflect method lets you use the compiler to create specializations, and get decent error messages when you try to use new visitors or new types that don't have the correct handlers.
Perhaps the best thing about the Reflect system is that it's code, not data. That means you can add arbitrary special case code directly where it's needed, rather than trying to make the macro-cvar system handle everything.
Of course you can go farther and auto-generate your Reflect function, but in my experience manual maintenance is really not a bad problem. See previous notes :
Now, despite being pro-Reflect I thought I would look at some of the drawbacks.
1. Everything in headers. This is the standard C++ problem. If you truly want to be able to Reflect any class with any visitor, everything has to be in headers. That's annoying enough that in practice in a large code base you probably want to restrict to just a few types of visitor (perhaps just BinIO,TextIO), and provide non-template accessors for those.
This is a transformation that the compiler could do for you if C++ was actually well designed and friendly to programmers (grumble grumble).
That is, we have something like
but we don't want to eat all that pain, so we tell the compiler which types can actually ever visit us :
void Reflect( functor visit );
void Reflect( TextIO & visit );
void Reflect( BinIO & visit );
and then you can put all the details in the body. Since C++ won't do it for you, you have to do this by hand, and it's annoying boiler-plate,
but could be made easier with macros or autogen.
2. No virtual templates in C++. To call the derived-class implementation of Reflect you need to get down there in some ugly way. If you are specializing to just a few possible visitors (as above), then you can just make those virtual functions and it's no problem. Otherwise you need a derived-class dispatcher (see cblib and previous discussions).
3. Versioning. First of all, versioning in this system is not really any better or worse than versioning in any other system. I've always found automatic versioning systems to be more trouble than they're worth. The fundamental issue is that you want to be able to incrementally do the version transition (you should still be able to load old versions during development), so the objects need to know how to load old versions and convert them to new versions. So you wind up having to write custom code to adapt the old variables to the new, stuff like :
if ( version == 1 )
// used to have member m_angle
m_angleCos = cos(m_angle);
now, you can of course do this without explicit version numbers, which is my preference for simple changes. eg. when I have some text prefs
and decide I want to remove some values and add new ones, you can just leave code in to handle both ways for a while :
if ( visitor.IsRead() )
double m_angle = 0;
m_angleCos = cos(m_angle);
where I'm using the assumption that my IO visitor is a NOP on variables that aren't in the stream. (eg. when loading an old stream,
m_angleCos won't be found and so the value from m_angle will be loaded, and when loading a new stream the initial filling from m_angle
will be replaced by the later load from m_angleCos).
Anyway, the need for conversions like this has always put me off automatic versioning. But that also means that you can't use the auto-gen'ed reflection. I suspect that in large real-world code, you would wind up doing lots of little special case hacks which would prevent use of the simple auto-gen'ed reflection.
4. Note that macro-markup and Reflect() both could provide extra data, such as min & max value ranges, version numbers, etc. So that's not a reason to prefer one or the other.
5. Reflect() can be abused to call the visitor on values that are on the stack or otherwise not actually data members. Mostly that's a big advantage, it lets you do converions, and also serialize in a more human-friendly format (for text or tweakers) (eg. you might store a quaternion, but expose it to tweak/text prefs as euler angles) (*).
But, in theory with a macro-markup cvar method, you could use that for layout info of your objects, which would allow you to do more efficient binary IO (eg. by identifying big blocks of data that can be read in binary without any conversions).
(* = whenever you expose a converted version, you should also store the original form in binary so that write-then-read is a gauranteed nop ; this is of course true even for just floating point numbers that aren't printed to all their places, which is something I've talked about before).
I think this potential advantage of the cvar method is not a real advantage. Doing super-efficient binary IO should be as close to this :
void * data = Load( one file );
GameWorld * world = (GameWorld *) data;
as possible. That's going to require a whole different pathway for IO that's separate from the cvar/reflect pathway, so there's no need
to consider that as part of the pro/con.
6. The End. I've never used the Reflect() pattern in the real world of a large production codebase, so I don't know how it would really fare. I'd like to try it.