04-11-07 - 1

I did this Reflection + Prefs thing a while ago and I'm using it now and it's just so sweet I thought I'd write about it a bit.

It's in the Galaxy3 code which is here . (that version is old and my new stuff is better, I'll put it up some day)

There are a few components so I'll go through it. This is a very C++ template metaprogramming thing. When I wrote it I was constrained by the limited features of VC6, and it works in VC6, but if you use the fully compliant spec you can do even nicer things.

"Reflection". This is just a convention. A lot of template metaprogramming is just conventions. Anyway, this convention says : any reflectable class defines a member function called "Reflection" which reflects all its members. The Reflection function takes a templated visitor so it can be visited by any functor, and it just calls that visitor on all its members (with their name).

So, having this convention is cool already, because if you implement Reflection in all your classes you can now do an "IOZ" type of automatic serialization system and you can serialize any class which implements Reflection.

In this case we're going to use Reflection to do "Pref" IO. Prefs in my world are text files like INI files, that have a bunch of lines something like "member : data" in nice readable and editable formats.

To read & write prefs we need to know how to read & write various data types to text. To do that, we define two templated functions : ReadFromText and WriteToText. You then specialize them to specific types and write the text IO for those types. For example to do ints you would supply functions that basically do printf and scanf. If you want you can implement these for your own arbitrary types, but you don't do that very often, only for new low-level types.

The default implementation of ReadFromText and WriteToText assume the unknown type is a complex type, so they write it in braces in the text file, and then call "PrefIO()" on that type so it can IO itself (in text).

PrefIO is just another convention. There are no implementations of PrefIO for basic types - if your type was basic it would have been IO'd by ReadFromText/WriteToText. The default implementation of PrefIO just calls Reflection !! Okay, now this is where the magic is starting to happen. Default PrefIO assumes the type is a complex type that implements Reflection, so we just pass in the IOText functor which is automatically called on all the members. (another case where you might want to implement the PrefIO function is if your type is an opaque class with a pImpl - you don't want to expose Reflection publicly so you just expose a PrefIO function which can then call through to work on the hidden pImpl).

Now, there are some types which are neither basic (and get handled by RFT/WTT) , nor are their Reflected. One example is any complex type that you don't own the code to, so you simply can't add a Reflection() member. For example, types provided by another library such as Windows. You still want to do PrefIO on those types, well you can - you just specialize the PrefIO template for those types and manually call the members instead of having Reflection do it for you.

Okay, so what's so cool about this system? Well, for doing basic stuff it's not particularly better or worse than any other reflection/member-IO system. In any system like that you have to mark up a list of your members somewhere and we have to do that too. (I'm ignoring the fact that you can autogenerate that list by parsing headers, since you can do that in any system, and you still have to mark it up by flagging which members to reflect and which not to).

The cool thing is what happens when you encounter a type that the system isn't familiar with. In traditional prefs text IO systems, first of all that error might not be detected until run-time, which is nasty. Second of all, to support IO for a new type you often have to define some new MetaData object that describes that type, and add it into the Pref system.

In this system when you add some new type to a class, you will have a Reflection() in that class that acts on the new type. When you write code that treats that class as a pref, it will try to compile the PrefIO functions, which will produce a compile error that looks something like :

c:\src\cblib\Reflection.h(211) : error C2228: left of '.Reflection' must have class/struct/union type
        type is 'COLORREF'

You're getting this error because it's falling down to the backup implementation for unknown types, which is to assume they're complex and call Reflection() on them. Since this type doesn't have Reflection you get an error. Now you can fix that either by -

1. if it is your class, you just implement Reflection()

2. if it's a basic type that you just don't have an IO for you, you write ReadFromText/WriteToText. If these are found they are used first and it doesn't even call to PrefIO.

3. else it's some sort of struct or type you don't own, (like in this example), so you write a PrefIO, which overrides the default so Reflection doesn't get called.

Okay, the other really great thing about this is what happens any time it breaks for whatever reason. You have some weirdo ill-behaved class and the standard generic Reflection doesn't work. Well, your Reflection() function is just code and there's no reason you can't just go in there and write extra code to fix whatever is wrong. In the most severe case you could specialize Reflection to specific visitors to special-case those, but more often you can fix it by just doing some time addition to normal reflection.

IMHO there's also a big cognitive benefit to having a system where the compiler is doing the book-keeping for you (errors are compile time and the compiler tells you if you don't support certain types), and where the system is "just code" as opposed to some custom metadata or parsing system which will have its own quirks and ways of working that you'll have to learn.

No comments:

old rants