Creating a meta-object system for C++

A while ago I developed a meta-object system for C++ to use on a game engine I started making, but it is now obsolete due to converting the engine to C# (I’ll make a post about that.. someday). This post was supposed to be the first part of the game engine development posts, but since it’s not part of the engine, it’ll be on it’s own now.

Do keep in mind that this was not designed to be a general-purpose meta-object system. It was designed for a game engine. It’s a relatively simple system, but it is very useful for serialization and class metadata. This is also incomplete, so features such as array property types are not present, and the code is taken from the engine, so it does not work as-is. It is merely a demonstration of how a meta-object system can be done. But hopefully this will be of some use to someone.

Full code

Some of the code is not shown here and most of it is split into parts, but you can inspect the full code here.

Meta-objects?

Say you need to access data and metadata of a C++ class without actually knowing the type itself, during runtime. This mechanism is often referred to as reflection or Run-time type information (RTTI), although RTTI refers to introspection, while reflection refers to both introspection and modification. Unlike in higher-level languages such as C# and Java, reflection is not built-in in C++. And while there are multiple different libraries supporting reflection for C++, many of them are too complex for the needs of a simple property system.

Reflection can be used for serialization and providing additional metadata about classes. In the case of a game engine, we can use reflection to serialize properties of game objects that we add into a scene, and provide human-readable labels for our properties in the SDK. These use cases rely on not having to know the exact type of an object during compile-time; that they can read and modify the object during run-time regardless of its type.

Our reflection system is made up of several parts.

  • WObject. Class that all objects using the reflection system need to inherit.
  • Repository. This is a singleton that contains all meta-objects.
  • Meta-objects. These are used to access the metadata of a certain class.
  • Properties. These are basically object variables that are exposed to the reflection system.
  • Inputs. Object methods that can be invoked via the reflection system.
  • Outputs. Variables of type WOutput. These are not properties because they are treated very differently within the engine.
  • Attributes. These are simple text values that we can attach to a certain type. Can be used to provide additional data about a type.

I will talk about properties, inputs and outputs and how they are used in the game engine series. For now, all you need to know is that properties are variables, inputs are methods and outputs are variables of a specific type, WOutput.

Repository

Let’s start with the easiest class to write; The repository. It is basically a container that can be used to find meta-objects. The meta repository is a singleton, meaning only one instance of the object exists within the program runtime.

repository.h

#ifndef WMETAREPOSITORY_H
#define WMETAREPOSITORY_H

#include <vector>
#include <string>
#include "object.h"
#include "property.h"
#include "input.h"
#include "output.h"
#include "attribute.h"
#include "macros.h"

class WMetaRepository
{
public:
    static WMetaRepository *getInstance();

    WMetaRepository();

    int metaObjectCount() const;
    WMetaObject *metaObject(int index) const;

    WMetaObject *findByClassName(std::string name) const;

    void addMetaObject(WMetaObject *obj);

private:
    static WMetaRepository *sInstance;

    std::vector<WMetaObject*> mMetaObjects;
};

#endif // WMETAREPOSITORY_H

repository.cpp

#include "repository.h"

WMetaRepository *WMetaRepository::sInstance = 0;

WMetaRepository *WMetaRepository::getInstance()
{
    if(!sInstance)
        sInstance = new WMetaRepository();
    return sInstance;
}

WMetaRepository::WMetaRepository()
{

}

int WMetaRepository::metaObjectCount() const
{
    return mMetaObjects.size();
}

WMetaObject *WMetaRepository::metaObject(int index) const
{
    return mMetaObjects[index];
}

WMetaObject *WMetaRepository::findByClassName(std::string name) const
{
    for(int i = 0; i < mMetaObjects.size(); i++) {
        WMetaObject *obj = mMetaObjects[i];
        if(obj->name() == name)
            return obj;
    }
    return 0;
}

void WMetaRepository::addMetaObject(WMetaObject *obj)
{
    if(findByClassName(obj->name())) {
        return;
    }
    mMetaObjects.push_back(obj);
}

We can also add iterator methods to support iterator loops for meta objects, but this will do for now. findByClassName is a method for finding a meta-object by giving it a class name. It is especially useful when deserializing objects, as we save the type of our object as text.

Note that all classes we are going to make are prefixed with W. This is just a naming convention for the game engine. The right thing to do would be to use namespaces, but the classes are a little bit easier to use without namespaces. It’s a design decision, up to you.

MetaObject

Next up, the MetaObject class. We’ll go through the function definitions one by one.

object.h

#ifndef WMETAOBJECT_H
#define WMETAOBJECT_H

#include "../global.h"
#include <string>
#include <vector>

class WMetaInfo;
class WMetaProperty;
class WMetaInput;
class WMetaOutput;
class WMetaAttribute;

/**
 * @brief Provides metadata about a WObject class.
 */
class WMetaObject
{
public:
    typedef void*(*DefaultConstructorFunc)();

    WMetaObject(std::string name, std::string baseClassName, DefaultConstructorFunc defaultConstructor,
                std::vector<WMetaInfo*> info);
    ~WMetaObject();

    std::string name() const;

    std::string baseClassName() const;
    WMetaObject *baseClass() const;

    bool isSubclassOf(std::string type) const;

    void *createInstance();

    std::vector<WMetaProperty*>::const_iterator propertiesBegin() const;
    std::vector<WMetaProperty*>::const_iterator propertiesEnd() const;

    /** Find property meta info with the given name. Searches recursively from base classes as well. */
    WMetaProperty *getProperty(std::string name) const;

    std::vector<WMetaInput*>::const_iterator inputsBegin() const;
    std::vector<WMetaInput*>::const_iterator inputsEnd() const;

    /** Find input meta info with the given name. Searches recursively from base classes as well. */
    WMetaInput *getInput(std::string name);

    std::vector<WMetaOutput*>::const_iterator outputsBegin() const;
    std::vector<WMetaOutput*>::const_iterator outputsEnd() const;

    /** Find output meta info with the given name. Searches recursively from base classes as well. */
    WMetaOutput *getOutput(std::string name);

    std::vector<WMetaAttribute*>::const_iterator attributesBegin() const;
    std::vector<WMetaAttribute*>::const_iterator attributesEnd() const;

    /** Get value of an attribute of given name. NOT recursive. */
    std::string getAttribute(std::string name) const;

private:
    std::string mName;
    std::string mBaseClassName;
    DefaultConstructorFunc mDefaultConstructor;
    std::vector<WMetaProperty*> mProperties;
    std::vector<WMetaInput*> mInputs;
    std::vector<WMetaOutput*> mOutputs;
    std::vector<WMetaAttribute*> mAttributes;
};

class WMetaInfo
{
    public:
        enum Type {
            TYPE_PROPERTY,
            TYPE_INPUT,
            TYPE_OUTPUT,
            TYPE_ATTRIBUTE
        };

        WMetaInfo(Type type) : mInfoType(type), mMetaObject(0) {}

        Type infoType() const
        {
            return mInfoType;
        }

        WMetaObject *metaObject() const
        {
            return mMetaObject;
        }

    private:
        friend class WMetaObject;

        Type mInfoType;
        WMetaObject *mMetaObject;
};

#endif // WMETAOBJECT_H

First, let’s make the constructor.

WMetaObject::WMetaObject(std::string name, std::string baseClassName, DefaultConstructorFunc defaultConstructor,
                         std::vector<WMetaInfo *> info) :
    mName(name), mBaseClassName(baseClassName), mDefaultConstructor(defaultConstructor)
{
    for(int i = 0; i < info.size(); i++) {
        WMetaInfo *inf = info[i];
        inf->mMetaObject = this;
        switch(inf->infoType()) {
        case WMetaInfo::TYPE_PROPERTY:
            mProperties.push_back((WMetaProperty*)inf);
            break;
        case WMetaInfo::TYPE_INPUT:
            mInputs.push_back((WMetaInput*)inf);
            break;
        case WMetaInfo::TYPE_OUTPUT:
            mOutputs.push_back((WMetaOutput*)inf);
            break;
        case WMetaInfo::TYPE_ATTRIBUTE:
            mAttributes.push_back((WMetaAttribute*)inf);
            break;
        }
    }
    WMetaRepository *repo = WMetaRepository::getInstance();
    repo->addMetaObject(this);
}

Here we set the name, default constructor, base class and meta-info of the represented class. DefaultConstructorFunc is defined in the header file, and is simply a function pointer to a void function with no parameters. We store the base class as a string so that we can later use the repository to search for the meta-object of our base class. Meta-info is a collection of metadata attached to our meta-object, which includes properties, inputs, outputs and attributes.

The constructor adds all meta-info to their specified containers and adds the meta-object to the repository. The reason we need to add it in the constructor is that we need to create the meta-object outside function scope, and we only have a limited amount of statements we can do outside functions. Some may call this a dirty method, but it’s better than registering all of our metadata inside our main function.

Next, we have the destructor.

WMetaObject::~WMetaObject()
{
    for(auto it = mProperties.begin(); it != mProperties.end(); ++it) {
        if(*it) {
            delete *it;
        }
    }
    for(auto it = mInputs.begin(); it != mInputs.end(); ++it) {
        if(*it) {
            delete *it;
        }
    }
    for(auto it = mOutputs.begin(); it != mOutputs.end(); ++it) {
        if(*it) {
            delete *it;
        }
    }
    for(auto it = mAttributes.begin(); it != mAttributes.end(); ++it) {
        if(*it) {
            delete *it;
        }
    }
}

This is quite straightforward; we delete all meta-info as they are heap-allocated objects.

WMetaObject *WMetaObject::baseClass() const
{
    WMetaRepository *repo = WMetaRepository::getInstance();
    for(int i = 0; i < repo->metaObjectCount(); i++) {
        WMetaObject *obj = repo->metaObject(i);
        if(obj->name() == mBaseClassName)
            return obj;
    }
    return 0;
}

bool WMetaObject::isSubclassOf(std::string type) const
{
    if(name() == type)
        return true;

    WMetaObject *bMeta = baseClass();
    while(true) {
        if(!bMeta)
            return false;
        if(bMeta->name() == type)
            return true;
        bMeta = bMeta->baseClass();
    }
}

These are utility functions for easily accessing class hierarchy from the meta-object.

WMetaProperty *WMetaObject::getProperty(std::string name) const
{
    for(int i = 0; i < mProperties.size(); i++) {
        WMetaProperty *prop = mProperties[i];
        if(prop->name() == name)
            return prop;
    }
    WMetaObject *b = baseClass();
    if(b)
        return b->getProperty(name);
    else
        return 0;
}

WMetaInput *WMetaObject::getInput(std::string name)
{
    for(int i = 0; i < mInputs.size(); i++) {
        WMetaInput *inp = mInputs[i];
        if(inp->name() == name)
            return inp;
    }
    WMetaObject *b = baseClass();
    if(b)
        return b->getInput(name);
    else
        return 0;
}

WMetaOutput *WMetaObject::getOutput(std::string name)
{
    for(int i = 0; i < mOutputs.size(); i++) {
        WMetaOutput *outp = mOutputs[i];
        if(outp->name() == name)
            return outp;
    }
    WMetaObject *b = baseClass();
    if(b)
        return b->getOutput(name);
    else
        return 0;
}

std::string WMetaObject::getAttribute(std::string name) const
{
    for(auto it = mAttributes.begin(); it != mAttributes.end(); ++it) {
        WMetaAttribute *att = *it;
        if(att->name() == name)
            return att->value();
    }
    return "";
}

These methods can be used to recursively search for different kinds of meta-info from the meta-object by name.

void *WMetaObject::createInstance()
{
    return mDefaultConstructor();
}

This can be used to create an instance of the object into heap-allocated memory. It simply calls the function pointer that was passed to the meta-object in the constructor.

The rest are quite self-explanatory methods that include getters and iterator getters.

std::string WMetaObject::name() const
{
    return mName;
}

std::string WMetaObject::baseClassName() const
{
    return mBaseClassName;
}

std::vector<WMetaProperty*>::const_iterator WMetaObject::propertiesBegin() const
{
    return mProperties.begin();
}

std::vector<WMetaProperty*>::const_iterator WMetaObject::propertiesEnd() const
{
    return mProperties.end();
}

std::vector<WMetaInput*>::const_iterator WMetaObject::inputsBegin() const
{
    return mInputs.begin();
}

std::vector<WMetaInput*>::const_iterator WMetaObject::inputsEnd() const
{
    return mInputs.end();
}

std::vector<WMetaOutput*>::const_iterator WMetaObject::outputsBegin() const
{
    return mOutputs.begin();
}

std::vector<WMetaOutput*>::const_iterator WMetaObject::outputsEnd() const
{
    return mOutputs.end();
}

MetaProperty

Now we’re getting to the more interesting parts of accessing unknown object types.

property.h

#ifndef WMETAPROPERTY_H
#define WMETAPROPERTY_H

#include <string>
#include "object.h"

enum WMetaPropertyType
{
    PROPERTY_BOOL,

    PROPERTY_FLOAT,
    PROPERTY_DOUBLE,

    PROPERTY_INT8,
    PROPERTY_INT16,
    PROPERTY_INT32,
    PROPERTY_INT64,

    PROPERTY_UINT8,
    PROPERTY_UINT16,
    PROPERTY_UINT32,
    PROPERTY_UINT64,

    PROPERTY_STRING,

    PROPERTY_VECTOR,
    PROPERTY_ANGLE,
    PROPERTY_QUATERNION,
    PROPERTY_TRANSFORM,
    PROPERTY_MATRIX,
    PROPERTY_COLOR,

    PROPERTY_UID,

    PROPERTY_OBJECT,

    // Special property type, with very limited use
    PROPERTY_CUSTOM
};

enum WMetaPropertyFlags
{
    PFLAGS_NONE = 0, // No flags
    PFLAGS_EDITABLE = (1 << 0), // Property is editable within the SDK
    PFLAGS_ARRAY = (1 << 1), // Property is an array of the given type, and not a single instance
    PFLAGS_SERIALIZEABLE = (1 << 2), // Property can be serialized
};

class WINDENGINE_DLL WMetaProperty : public WMetaInfo
{
    public:
        WMetaProperty(std::string name, WMetaPropertyType type, int flags, int offset, int size);
        WMetaProperty(std::string name, std::string objectType, int flags, int offset, int size);

        std::string name() const;
        WMetaPropertyType type() const;

        /**
         * @brief Return object type name. Only useful if property type is OBJECT
         * @return
         */
        std::string objectType() const;

        int flags() const;

        int offset() const;
        int size() const;

        void getValue(void *obj, void *buf);
        void setValue(void *obj, void *buf);

    private:
        std::string mName;
        WMetaPropertyType mType;
        std::string mObjectType;
        int mFlags;
        int mOffset;
        int mSize;
};

#endif // WMETAPROPERTY_H

Take note of the enumeration types. WMetaPropertyType defines all different types of properties that we can have. It includes all basic types, strings and custom structures like vectors, transforms and matrices, which we’ll be going through in another part of the game engine series. Basically, you add all the different data types that you want to support here. WMetaPropertyFlags define other attributes that we may want to define for a property, such as whether the property is editable within the SDK, whether it is serializable, and whether it is an array instead of a single value.

void WMetaProperty::getValue(void *obj, void *buf)
{
    switch(mType) {
    case PROPERTY_STRING:
    {
      std::string *str = (std::string*)buf;
      std::string *objStr = (std::string*)((char*)obj + mOffset);
      *str = *objStr;
      break;
    }
    //case PROPERTY_ENTITY:
    //case PROPERTY_COMPONENT:
    //case PROPERTY_ASSET:
    case PROPERTY_OBJECT:
    {
      WPointerBase *objSp = (WPointerBase*)((char*)obj + mOffset);
      WPointerBase *sp = (WPointerBase*)buf;
      sp->set(objSp->rawPointer());
      break;
    }
    default:
      memcpy(buf, (char*)obj + mOffset, mSize);
      break;
    }
}

void WMetaProperty::setValue(void *obj, void *buf)
{
    switch(mType) {
    case PROPERTY_STRING:
    {
      std::string *str = (std::string*)buf;
      std::string *objStr = (std::string*)((char*)obj + mOffset);
      *objStr = *str;
      break;
    }
    //case PROPERTY_ENTITY:
    //case PROPERTY_COMPONENT:
    //case PROPERTY_ASSET:
    case PROPERTY_OBJECT:
    {
      WPointerBase *objSp = (WPointerBase*)((char*)obj + mOffset);
      WPointerBase *sp = (WPointerBase*)buf;
      objSp->set(sp->rawPointer());
      break;
    }
    default:
      memcpy((char*)obj + mOffset, buf, mSize);
      break;
    }
}

This is where the magic of anonymously reading properties is. In the default switch case, which is used for most property types, the C method memcpy is used. It takes two pointers and one integer as arguments, being the source address, destination address and data size in bytes, respectively. So we are essentially copying raw data from one part of the memory to another. We know the offset of the property and it’s size in bytes, so we can take the object’s pointer, convert it to a char pointer and add the offset, giving us the memory address of the property.

However, reading a property like this assumes that the property is entirely stored on the object’s data structure. This is not always the case. For example, with the std::string type, the actual text is not stored within the object itself, it merely contains utility methods and a pointer to actual string. It’s essentially a C-string wrapper. So when memcpy is used to copy the value, we now have to strings pointing to the same C-string, which is not expected behavior for std::string. Strings delete their C-strings when their destructor is called, which will cause access violation if two strings point to the same C-string. The object type is also assumed to be a smart pointer type, which also cannot be raw copied.

#include "property.h"
#include "../util/pointer.h"
#include <cstring>
#include <memory>

WMetaProperty::WMetaProperty(std::string name, WMetaPropertyType type, int flags, int offset, int size)
    : WMetaInfo(TYPE_PROPERTY), mName(name), mType(type), mFlags(flags), mOffset(offset), mSize(size)
{
}

WMetaProperty::WMetaProperty(std::string name, std::string objectType, int flags, int offset, int size)
    : WMetaInfo(TYPE_PROPERTY), mName(name), mType(PROPERTY_OBJECT), mObjectType(objectType), mFlags(flags), mOffset(offset), mSize(size)
{

}

std::string WMetaProperty::name() const
{
    return mName;
}

WMetaPropertyType WMetaProperty::type() const
{
    return mType;
}

std::string WMetaProperty::objectType() const
{
    return mObjectType;
}

int WMetaProperty::flags() const
{
    return mFlags;
}

int WMetaProperty::offset() const
{
    return mOffset;
}

int WMetaProperty::size() const
{
    return mSize;
}

This is the rest of the property.cpp file. Just constructors and getters.

MetaInput, MetaOutput

Next we define two classes that hold metadata about inputs and outputs, which is a concept of event-driven functionality for the engine, similar to Source engine. An input is like a method that can be invoked, and all outputs connected to that input will be signaled that the input was called. Ideally, you would be able to link outputs to inputs both in code and in editor.

input.h

#ifndef WMETAINPUT_H
#define WMETAINPUT_H

#include "object.h"
#include "property.h"

class WINDENGINE_DLL WMetaInput : public WMetaInfo
{
    public:
        WMetaInput(std::string name, std::vector<WMetaPropertyType> parameterTypes);

        std::string name() const;

        int parameterCount() const;
        WMetaPropertyType getParameterType(int index) const;

        virtual void invoke(void *obj, void *p1 = 0) = 0;

    private:
        std::string mName;
        std::vector<WMetaPropertyType> mParameterTypes;
};

template<class C>
class WMetaInputNoParams : public WMetaInput
{
    public:
        typedef void(C::*FuncP)();

        WMetaInputNoParams(std::string name, std::vector<WMetaPropertyType> parameterTypes, FuncP methodPointer)
            : WMetaInput(name, parameterTypes), mMethodPointer(methodPointer)
        {
        }

        void invoke(void *obj, void *p1 = 0)
        {
            (((C*)obj)->*mMethodPointer)();
        }

    private:
        FuncP mMethodPointer;
};

template<class C, class T>
class WMetaInputOneParam : public WMetaInput
{
    public:
        typedef void(C::*FuncP)(T);

        WMetaInputOneParam(std::string name, std::vector<WMetaPropertyType> parameterTypes, FuncP methodPointer)
            : WMetaInput(name, parameterTypes), mMethodPointer(methodPointer)
        {
        }

        void invoke(void *obj, void *p1 = 0)
        {
            (((C*)obj)->*mMethodPointer)(*((T*)p1));
        }

    private:
        FuncP mMethodPointer;
};

#endif // WMETAINPUT_H

Here we have three classes; WMetaInput, WMetaInputNoParams and WMetaInputOneParam. A subclass of WMetaInput needs to be created for a number of parameters an input method can take, because template types need to be utilized for each parameter. More subclasses can be made, but I personally didn’t need more than one parameter for inputs.

Let’s look at the line of code that might’ve caught your eye.

(((C*)obj)->*mMethodPointer)(*((T*)p1));

What we’re basically doing here is invoking a method of obj, identified by mMethodPointer. obj and p1 are void pointers, while mMethodPointer is a function pointer. obj is casted to the pointer type that this input method is for, and p1 is first casted to the second template type and then dereferenced, invoking the method with the same value that p1 is pointing to.

input.cpp only defines getters and setters that were not defined in the header file.

output.h

#ifndef WMETAOUTPUT_H
#define WMETAOUTPUT_H

#include "object.h"
#include "property.h"

class WOutput;

class WINDENGINE_DLL WMetaOutput : public WMetaInfo
{
    public:
        WMetaOutput(std::string name, std::vector<WMetaPropertyType> parameterTypes, int offset);

        std::string name() const;
        int offset() const;

        int parameterCount() const;
        WMetaPropertyType getParameterType(int index) const;

        WOutput *getReference(void *obj) const;

    private:
        std::string mName;
        std::vector<WMetaPropertyType> mParameterTypes;
        int mOffset;
};

#endif // WMETAOUTPUT_H

output.cpp

#include "output.h"
#include "../events/woutput.h"

WMetaOutput::WMetaOutput(std::string name, std::vector<WMetaPropertyType> parameterTypes, int offset)
    : WMetaInfo(TYPE_OUTPUT), mName(name), mParameterTypes(parameterTypes), mOffset(offset)
{
}

std::string WMetaOutput::name() const
{
    return mName;
}

int WMetaOutput::offset() const
{
    return mOffset;
}

int WMetaOutput::parameterCount() const
{
    return mParameterTypes.size();
}

WMetaPropertyType WMetaOutput::getParameterType(int index) const
{
    return mParameterTypes[index];
}

WOutput *WMetaOutput::getReference(void *obj) const
{
    return (WOutput*)((char*)obj + mOffset);
}

WMetaOutput is very similar to WMetaProperty, since it refers to an object variable of type WOutput, so for example, getReference is a similar method to WMetaProperty‘s getValue.

WMetaAttribute in attribute.h defines metadata for attributes, which are basically static data attached to classes. For example, components can use requiredComponent attribute to describe what other components that component depends on.

Macros

Finally, we define some macros to make the use of this meta-object system less tedious.

macros.h

#ifndef WMETAMACROS_H
#define WMETAMACROS_H

#define META_OBJECT_INIT(C, BC) \
    public: \
        typedef BC BaseClass; \
        virtual WMetaObject *metaObject() const; \
        static WMetaObject *staticMetaObject(); \
        static std::vector<WMetaInfo*> getMetaInfo(); \
    private: \
        static WMetaObject sMetaObject;

#define META_OBJECT_REGISTER(C, BC) \
    WMetaObject *C::metaObject() const { \
        return &C::sMetaObject; \
    } \
    WMetaObject *C::staticMetaObject() { \
        return &sMetaObject; \
    } \
    static void *getDefaultConstructorOf##C() { \
        return new C(); \
    } \
    WMetaObject C::sMetaObject(#C, #BC, &getDefaultConstructorOf##C, C::getMetaInfo());

#define META_INFO_BEGIN(C) \
    std::vector<WMetaInfo*> C::getMetaInfo() { \
        typedef C ThisClass; \
        C inst; \
        std::vector<WMetaInfo*> metaInfo; \

#define META_INFO_END() \
        return metaInfo; \
    }

#define META_PROPERTY(Name, Type, Flags) \
    metaInfo.push_back(new WMetaProperty(#Name, Type, Flags, \
    (char*)&inst.Name - (char*)&inst, sizeof(inst.Name)));

#define META_PROPERTY_OBJECT(Name, Flags) \
    metaInfo.push_back(new WMetaProperty(#Name, inst.Name.type(), Flags, \
    (char*)&inst.Name - (char*)&inst, sizeof(inst.Name)));

#define META_INPUT_NO_PARAMS(Name) \
    std::vector<WMetaPropertyType> pTypesFor##Name; \
    metaInfo.push_back(new WMetaInputNoParams<ThisClass>(#Name, \
        pTypesFor##Name, &ThisClass::Name));

#define META_INPUT_ONE_PARAM(Name, Type1, ActualType1) \
    std::vector<WMetaPropertyType> pTypesFor##Name; \
    pTypesFor##Name.push_back(Type1); \
    metaInfo.push_back(new WMetaInputOneParam<ThisClass, ActualType1>(#Name, \
        pTypesFor##Name, &ThisClass::Name));

#define META_OUTPUT_NO_PARAMS(Name) \
    std::vector<WMetaPropertyType> pTypesFor##Name; \
    metaInfo.push_back(new WMetaOutput(#Name, pTypesFor##Name, (char*)&inst.Name - (char*)&inst));

#define META_OUTPUT_ONE_PARAM(Name, Type1) \
    std::vector<WMetaPropertyType> pTypesFor##Name; \
    pTypesFor##Name.push_back(Type1); \
    metaInfo.push_back(new WMetaOutput(#Name, pTypesFor##Name, (char*)&inst.Name - (char*)&inst));

#define META_ATTRIBUTE(Name, Value) \
    metaInfo.push_back(new WMetaAttribute(#Name, Value));

#endif // WMETAMACROS_H

Conclusion

Honestly, this blog post was kind of rushed in the end. I started writing this when I was making the meta-object system, and it kind of stopped when I moved to my C# solution. Then summer came, and I pretty much forgot about it. But it was halfway done, so I decided to finish it anyway, hoping it will be useful to someone.

I won’t stop blogging entirely, but I’m not going to be making posts very often. I only have a few interesting topics I want to write about, and making those posts will take time.

Thanks for reading. Feel free to comment about typos or suggestions that I could fix or add into this post, if you have any.