In this article I'll talk about type deduction and member reflection, both of which are critical building blocks for everything else.
First up is type deduction. When using the templated MetaCreator class:
template <typename Metatype>
class MetaCreator
{
public:
MetaCreator( std::string name, unsigned size )
{
Init( name, size );
}
static void Init( std::string name, unsigned size )
{
Get( )->Init( name, size );
}
// Ensure a single instance can exist for this class type
static MetaData *Get( void )
{
static MetaData instance;
return &instance;
}
};
Whenever you pass in a const, reference, or pointer qualifier an entire new templated MetaCreator will be constructed by the compiler. This just won't do, as we don't want the MetaData of a const int to be different at all from an int, or any other registered type. There's a simple, yet very quirky, trick that can solve all of our problems. Take a look at this:
//
// RemQual
// Strips down qualified types/references/pointers to a single unqualified type, for passing into
// a templated type as a typename parameter.
//
template <typename T>
struct RemQual
{
typedef T type;
};
template <typename T>
struct RemQual<const T>
{
typedef T type;
};
template <typename T>
struct RemQual<T&>
{
typedef T type;
};
template <typename T>
struct RemQual<const T&>
{
typedef T type;
};
template <typename T>
struct RemQual<T&&>
{
typedef T type;
};
template <typename T>
struct RemQual<T *>
{
typedef T type;
};
template <typename T>
struct RemQual<const T *>
{
typedef T type;
};
I'm actually not familiar with the exact terminology to describe what's going on here, but I'll try my best. There's many template overloads of the first RemQual struct, which acts as the "standard". The standard is just a single plain type T, without any qualifiers and without pointer or reference type. The rest of the templated overloaded version all contain a single typedef which lets the entire struct be used to reference a single un-qualified type by supplying any of the various overloaded types to the struct's typename param.
Overloads for the R-value reference must be added as well in order to strip down to the bare type T.
Now that we have our RemQual (remove qualifiers) struct, we can use it within our META macros to refer to MetaData types. Take a look at some example re-writes of the three META macros:
//
// META_TYPE
// Purpose: Retrieves the proper MetaData instance of an object by type.
//
#define META_TYPE( TYPE ) (MetaCreator<RemQual<TYPE>::type>::Get( ))
//
// META
// Purpose: Retrieves the proper MetaData instance of an object by an object's type.
//
#define META( OBJECT ) (MetaCreator<RemQual<decltype( OBJECT )>::type>::Get( ))
//
// META_STR
// Purpose : Finds a MetaData instance by string name
//
#define META_STR( STRING ) (MetaManager::Get( STRING ))
//
// DEFINE_META
// Purpose : Defines a MetaCreator for a specific type of data
//
#define DEFINE_META( TYPE ) \
MetaCreator<RemQual<TYPE>
::type> NAME_GENERATOR( )( #TYPE, sizeof( TYPE ) )
The idea is you feed in the typedef'd type from RemQual into the MetaCreator typename param. This is an example of using macros well; there's no way to screw up the usage of them, and they are still very clean and easy to debug as there isn't really any abuse going on. Feel free to ignore specific META macros you wouldn't actually use. I use all three META_TYPE, META and META_STR. It's a matter of personal preference on what you actually implement in this respect. It will likely be pretty smart to place whatever API is created into a namespace of it's own, however.
And that covers type deduction. There are some other ways of achieving the same effect, like partial template specialization as covered here, though I find this much simpler.
Next up is register members of structures or classes with the MetaData system. Before anything continues, lets take a look at an example Member struct. A Member struct is a container of the various bits of information we'd like to store about any member:
//
// Member
// Purpose: Stores information (name and offset of member) about a data member of a specific class. Multiple
// Member objects can be stored in MetaData objects within a std::vector.
//
class Member
{
public:
Member( std::string string, unsigned val, MetaData *meta );
~Member( );
const std::string &Name( void ) const; // Gettor for name
unsigned Offset( void ) const; // Gettor for offset
const MetaData *Meta( void ) const; // Gettor for data
private:
std::string name;
unsigned offset;
const MetaData *data;
};
This member above is actually almost exactly what implementation I have in my own reflection as it stands while I write this; there's not a lot needed. You will want a MetaData instance to describe the type of data contained, a name identifier, and an unsigned offset representing the member's location within the containing object. The offset is exceptionally important for automated serialization, which I'll likely be covering after this article.
The idea is that a MetaData instance can contain various member objects. These member objects are contained within some sort of container (perhaps std::vector).
In order to add a member we'll want another another very simple macro. There are two big reasons a macro is efficient in this situation: we can use stringize; there's absolutely no way for the user to screw it up.
Before showing the macro I'd like to talk about how to retrieve the offset. It's very simple. Take the number zero, and turn this into a pointer to a type of object (class or struct). After the typecasting, use the -> operator to access one of the members. Lastly, use the & operator to retrieve the address of the member's location (which will be offset from zero by the -> operator) and typecast this to an unsigned integer. Here's what this looks like:
#define ADD_MEMBER( MEMBER ) \
AddMember( #MEMBER, (unsigned)(&(NullCast( )->MEMBER)), META( NullCast( )->MEMBER ))
This is quite the obtrusive line of code we have here! This is also a good example of a macro used well; it takes a single parameter and applies it to multiple locations. There's hardly any way for the user of this macro to screw up.
NullCast is a function I'll show just after this paragraph. All it does is return a pointer to NULL (memory address zero) of some type. Having this type pointer to address zero, we then use the ADD_MEMBER macro to provide the name of a member to access. The member is then accessed, and the & operator provides an address to this member with an offset from zero. This value is then typecasted to an unsigned integer and and passed along to the AddMember function within the macro. The stringize operator is also used to pass a string representation of the member to the AddMember function, as well as a MetaData instance of whatever the type of data the member is.
Now where does this AddMember function actually go? Where is it from? It's actually placed into a function definition. The function AddMember itself resides within the MetaCreator. This allows the MetaCreator to call the AddMember function of the MetaData instance it holds, which then adds the Member object into the container of Members within the MetaData instance.
Now, the only place that this AddMember function can be called from, building from the previous article, is within the MetaCreator's constructor. The idea is to use the DEFINE_META macro to also create a definition of either the MetaCreator's constructor, or a MetaCreator method that is called from the MetaCreator's constructor. Here's an example:
DEFINE_META( GameObject )
{
ADD_MEMBER( ID );
ADD_MEMBER( active );
ADD_MEMBER( components );
}
As you can see this formation is actually very intuitive; it has C++-like syntax, and it's very clear what is going on here. A GameObject is being registered in the Meta system, and it has members of ID, active, and components being added to the Meta system. For clarity, here's what the GameObject's actual class definition might look like (assuming component based architecture):
class GameObject
{
public:
// Sends all components within this object the same message
void SendMessage( Message *msg );
bool HasComponent( std::string& name ) const;
void AddComponent( std::string componentType );
Component *GetComponent( std::string name ) const;
handle GetID( void ) const;
handle ID;
// This boolean should always be true when the object is alive. If this is
// set to false, then the ObjectFactory will clean it up and delete this object
// during its inactive sweep when the ObjectFactory's update is called.
bool active;
std::vector components;
private:
// Only to be used by the factory!
GameObject( ) : ID( -1 ), active( true ) {}
~GameObject( );
};
Now lets check out what the new DEFINE_META macro could look like:
#define DEFINE_META( TYPE ) \
MetaCreator<RemQual<TYPE>::type> NAME_GENERATOR( )( #TYPE, sizeof( TYPE ) ); \
void MetaCreator<RemQual<TYPE>::type>::RegisterMetaData( void )
The RegisterMetaData declaration is quite peculiar, as the macro just ends there. What this is doing is setting up the definition of the RegisterMetaData function, so that the ADD_MEMBER macro calls are actually lines of code placed within the definition. The RegisterMetaData function should be called from the MetaCreator's constructor. This allows the user to specify what members to reflect within a MetaData instance of a particular type in a very simple and intuitive way.
Last but not least, lets talk about the NullCast function real quick. It resides within the MetaCreator, as NullCast requires the template's typename MetaType in order to return a pointer to a specific type of data.
And that's that! We can now store information about the members of a class and deduce types from objects in an easily customizeable way.
Here's a link to a demonstration program, compileable in Visual Studio 2010. I'm sure this could compile in GCC with a little bit of house-keeping as well, but I don't really feel like doing this as I need to get to bed! Here's the output of the program. The format is <type> <size>. For the object, members and their offsets are printed:
int
4
float
4
std::string
28
Object
8
{
ID
0
active
4
}
Now you might notice at some point in time, you cannot reflect private data members! This detail will be covered in a later article. The idea behind it is that you require source code access to the type you want to reflect, and place a small tiny bit of code inside to gain private data access. Either that or make the MetaCreator a friend class (which sounds like a messy solution to me).
And here we have all the basics necessary for automated serialization! We can reflect the names of members, their types, and offsets within an object. This lets the reflection register any type of C++ data within itself.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.