Showing posts with label serialization. Show all posts
Showing posts with label serialization. Show all posts

Friday, August 20, 2010

QMetaEnum Magic - Serializing C++ Enums - Take 2

A few posts ago I described two methods of serializing C++ enums. Of these, method 2 serialized the Qt::Key enum. The approach, however, relied on some behind the scenes magic that I wasn't fully aware of nor did I fully document. This approach remedies that and describes in full the requirements for serializing C++ enums.

Method 2 - Reevaluated

The goal for this method is to take an enum called MyKey in the MyNS namespace and serialize it. In brief, the code for that looks like the following:

namespace MyNS
{
    enum MyKey {
        MyKey_Return = 0,
        MyKey_Enter = 1
        };
}

Since we want Qt to be able to serialize the above enum, it needs to know about the enum, so we're going to need Q_ENUMS(MyKey). But unless moc sees a Q_OBJECT or Q_GADGET macro, our Q_ENUMS macro will result in a compilation error. To get around this, we need to convince moc to process the file as if it were a class. We can do that by adding some preprocessor defines:

#ifndef Q_MOC_RUN
namespace MyNS
#else
class MyNS
#endif
{
#if defined(Q_MOC_RUN)
    Q_GADGET
    Q_ENUMS(MyKey)
public:
#endif
    enum MyKey {
        MyKey_Return = 0,
        MyKey_Enter = 1
    };
}

At this point we've convinced moc to look at and process the file, but that alone isn't enough. The code that moc generates assumes that a const staticMetaObject has been declared, but at this point one hasn't been declared. Although some compilers will let us get away with this, we'll declare it as follows:

    // ... continuing at the enum
    enum MyKey {
        MyKey_Return = 0,
        MyKey_Enter = 1
    };
    extern const QMetaObject staticMetaObject;
}

With that in place, we're ready to serialize the enum. The first step is to get a copy of the QMetaEnum object. We do so by accessing the static reference directly and then calling indexOfEnumerator to get the appropriate index:

    // get the QMetaEnum object
    const QMetaObject &mo = MyNS::staticMetaObject;
    int enum_index = mo.indexOfEnumerator("MyKey");
    QMetaEnum metaEnum = mo.enumerator(enum_index);

With the QMetaEnum instance in hand, we can now serialize the enum as demonstrated in my prior post:

    // convert to a string
    MyNS::MyKey key = MyNS::MyKey_Return;
    QByteArray str = metaEnum.valueToKey(key);
    qDebug() << "Value as str:" << str;

    // convert from a string
    int value = metaEnum.keyToValue("MyKey_Enter");
    key = static_cast(value);
    qDebug() << "key is MyKey_Enter? : " << (key == MyNS::MyKey_Enter);

With all the above in place, we've used a bit of magic to trick moc into thinking MyNS was a class. This causes moc to generate a MyNS::staticMetaObject instance and store the necessary serialization meta data. With everything in place, we get the following output:

Value as str: "MyKey_Return" 
key is MyKey_Enter? :  true 

References:

Wednesday, July 28, 2010

QMetaEnum Magic - Serializing C++ enum's

Qt has a number of useful classes and utilities; among these is QMetaEnum which provides the ability to serialize and deserialize C++ enumerations through the use of moc, the meta-object compiler.

Method 1 - Enum's within a QObject (fairly common)

First, let's take a look at a common example of an enum within a class as provided in the "Secrets of Qt Full" developer days presentation:

class Person : public QObject {
    Q_OBJECT
    enum  Qualification { Student, ... };
    Q_ENUMS(Qualification)
};

When moc runs on the above and sees Q_OBJECT it adds a staticMetaObject member (that's static, big surprise) of type QMetaObject. This QMetaObject instance has an indexOfEnumerator and an enumerator member functions that make it possible to access the QMetaEnum representing the Person::Qualification enum.

The code to access the QMetaEnum member looks something like the following:

const QMetaObject &mo = Person::staticMetaObject;
int index = mo.indexOfEnumerator("Qualification"); // watch out during refactorings
QMetaEnum metaEnum = mo.enumerator(index);

We can then use the QMetaEnum object as follows:

// first, let's convert from an enum value to a string
Qualification q = Person::Student;
QByteArray str = metaEnum.valueToKey(q);
// str now contains "Student"

// second, let's convert from a string to an enum value:
int value = metaEnum.keyToValue("Student");
Qualification q = static_cast(value);

Method 2 - Without a QObject-based class (less common)

UPDATE: Based on Vladislaw's comment, I've done further research and added an additional post which demonstrates how to accomplish this in namespaces other than Qt.

Another less well-known feature is that with only minor effort you can use this same feature without needing a QObject-based class as a container. For this example, I'll use the Qt::Key enum. Since we don't want a QObject based class, rather than using Q_OBJECT we'll use the Q_GADGET macro within a container:

class Container {
 Q_GADGET
 Q_PROPERTY(Qt::Key key_enum);
public:
 Qt::Key key_enum;
};

Like Q_OBJECT, Q_GADGET creates a staticMetaObject member that we'll need to use to access the QMetaEnum:

const QMetaObject &mo = Container::staticMetaObject;
int prop_index = mo.indexOfProperty("key_enum");
QMetaProperty metaProperty = mo.property(prop_index);
QMetaEnum metaEnum = metaProperty.enumerator();

Unlike the first example, in this example I referenced the Qt::Key typed key_enum property to get a hold on the QMetaEnum object that I can now use exactly as before:

// convert to a string
Qt::Key key = Qt::Key_Down;
QByteArray str = metaEnum.valueToKey(key);
qDebug() << "Value as str:" << str;

// convert from a string
int value = metaEnum.keyToValue("Key_Up");
key = static_cast(value);
qDebug() << "key is Key_Up: " << (key == Qt::Key_Up);

Which results in the following output:

Value as str: "Key_Down" 
key is Key_Up:  true

And there you have it... an easy way to leverage Qt to serialize C++ enums.