Blog

Object factory

Before I continue the series about data serialization in Qt, there is one important related topic than needs to be mentioned first: the ability to dynamically create an object based on a class name.

Let's assume that we want to serialize a list of shapes. The Shape is an abstract class and actual objects stored in the list have various derived classes: Rectangle, Circle, etc. During serialization, we can save the class name and the object data for each item. During deserialization, we need to be able to create an instance of an appropriate class. This is where an object factory becomes necessary. In languages like C# or Java, which support reflection, it is possible to instantiate a class given as a string with a few lines of code. But in C++ there is no such mechanism.

The simple solution is to create a single function with a large switch (or series of ifs) that creates the object of an appropriate type. It's not particularly elegant and it breaks the object oriented design, but in many cases it's acceptable. However, when you have lots of classes that are scattered through different parts of the application, it may become hard to manage. And if the application has external modules or dynamically loaded plug-ins, it becomes even more difficult.

A much more elegant solution is when the factory is abstract and it doesn't know anything about the objects it creates. Instead, classes that can be instantiated using the factory have to be registered using some type of internal map. This way, each module or plug-in can independently register its own set of classes.

Qt has two mechanism that can be useful for creating such factories. They may seem similar, but in fact there are major differences.

QMetaType
The construct() method can be used to create an instance of any built-in type, or a custom type for which the Q_DECLARE_METATYPE macro was specified. This is what a QVariant does internally to wrap an custom type. However, this mechanism is designed to be used for value types, i.e. classes that have default constructors and copy constructors. It doesn't make any sense for objects of abstract classes, which are almost always passed by pointer, and often have their copy constructors disabled.
QMetaObject
The newInstance() method can be used to create an instance of any class that inherits QObject. The only condition is that the constructor needs to be explicitly declared with the Q_INVOKABLE modifier. This works fine with polymorphic objects, because QObject is often used as a base class for various abstract hierarchies of classes (for example widgets). Note that since Qt 4 it is not possible to retrieve a QMetaObject based on the class name without some additional work.

It is very easy to create an object factory which relies on QMetaObject. An example can be seen here. However, this solution has a few disadvantages:

  • The constructor needs to be explicitly declared with Q_INVOKABLE in order to be accessible from the QMetaObject.
  • There is no compile time check whether the appropriate constructor exists and is accessible, or whether parameter types are correct. You will only get a runtime warning when you actually try to create the instance and NULL will be returned instead.
  • Subclassing QObject adds some memory footprint to each object instance. Also dynamic method calls used by QMetaObject have some overhead as run-time type checking has to be performed.

However it's not difficult to create a custom factory for classes of any type, that doesn't have these limitations. An example of a custom factory that works for any class that inherits QObject can be seen below:

class ObjectFactory
{
public:
    template<typename T>
    static void registerClass()
    {
        constructors().insert( T::staticMetaObject.className(), &constructorHelper<T> );
    }

    static QObject* createObject( const QByteArray& className, QObject* parent = NULL )
    {
        Constructor constructor = constructors().value( className );
        if ( constructor == NULL )
            return NULL;
        return (*constructor)( parent );
    }

private:
    typedef QObject* (*Constructor)( QObject* parent );

    template<typename T>
    static QObject* constructorHelper( QObject* parent )
    {
        return new T( parent );
    }

    static QHash<QByteArray, Constructor>& constructors()
    {
        static QHash<QByteArray, Constructor> instance;
        return instance;
    }
};

With this approach, there is no need to declare the constructor with Q_INVOKABLE. Also, if no appropriate constructor is found, a compile-time error will be reported in the constructorHelper() method as soon as the class is registered. This code is very easy to use:

ObjectFactory::registerClass<Foo>();

// ...

QObject* foo = ObjectFactory::createObject( "Foo" );

It is also easy to modify this code so that it works for custom abstract class hierarchies that do not inherit QObject. For example, instead of using the class name retrieved from the QMetaObject as a key, it can use a key of any type that is passed to the registerClass() method or retrieved automatically from a static class member. Also a different set of parameters can be passed to the constructor depending on the needs.

Filed under: Blog

Serialization in Qt - part 3

In the previous post I already wrote about backward and forward compatibility when serializing and deserializing data into a binary stream. Let's summarize:

  • backward compatibility - the ability to deserialize data serialized by and older version of the application
  • forward compatibility - the ability to deserialize data serialized by a newer version of the application

I said that backward compatibility can be achieve by storing a version tag in the data stream and conditionally changing the deserialization routine based on the version of data. However forward compatibility cannot be achieved this way because we can't predict what changes will be made in the future. This is fine for configuration data, but in case of documents it's not always acceptable.

The best solution would be to allow the application to skip and ignore information it doesn't understand, and extract as much information as it can. Note that it's not always possible. In case of a text document, the content can be preserved even if some fancy formatting is lost. However, let's recall the example in which we added child bookmarks to the Bookmark class. Even if we could skip loading the child bookmarks, we would still lose a lot of information, as only the top level bookmarks would be available in the old version. So before we start thinking about a fancy solution, we should first ask ourserlves if it's really worth the effort.

There is also a relatively simple workaround available. The new version of the application can be forced to save data in format compatible with an older version. This simply means that we have to add similar conditional code in serialization routines. Many applications work in this way, including MS Office applications. For example, the application could save all bookmarks in a linear fashion, losing the parent-child relationship, but still preserving all bookmarks.

But for true compatibility we need to design the data format in such way, that when the application encounters data that it doesn't understand, it can at least skip it and continue processing. But without additional metadata the application doesn't even know how many bytes it should skip.

A simple solution is to wrap all data in a QVariant before serializing, because QVariant writes a tag which identifies the type of the data before the actual data. Let's start with the following code:

template<typename T>
void operator <<( QVariant& data, const T& target )
{
    data = QVariant::fromValue<T>( target );
}

template<typename T>
void operator >>( const QVariant& data, T& target )
{
    target = data.value<T>();
}

These are generic function templates that convert any data to and from a variant. Now let's specialize these functions for our Bookmark type from the previous post. We will use a map to convert a bookmark to a variant and vice versa:

void operator <<( QVariant& data, const Bookmark& target )
{
    QVariantMap map;
    map[ "Name" ] << target.m_name;
    map[ "URL" ] << target.m_url;
    map[ "Children" ] << target.m_children;
    data << map;
}

void operator >>( const QVariant& data, Bookmark& target )
{
    QVariantMap map;
    data >> map;
    map[ "Name" ] >> target.m_name;
    map[ "URL" ] >> target.m_url;
    map[ "Children" ] >> target.m_children;
}

Note that because the bookmark object is converted to a QVariantMap before serializing, it can be successfully deserialized even if the application that reads the data doesn't know anything about the Bookmark type. What's more, we can add more elements to the map in the future without affecting either backward or forward compatibility. When reading a newer version of the file, the elements which are not understood are simply ignored. When reading an older version, missing elements are automatically replaced with default values for the given type.

When we try to compile the above code, we will receive a cryptic error similar to 'qt_metatype_id' : is not a member of 'QMetaTypeId<T>'. That's because a QList<Bookmark> cannot be converted into a QVariant. Since we know how to convert a Bookmark into a QVariant, we can easily convert a QList<Bookmark> into a QVariantList. This can even be done in a generic way:

template<typename T>
void operator <<( QVariant& data, const QList<T>& target )
{
    QVariantList list;
    list.reserve( target.count() );
    for ( int i = 0; i < target.count(); i++ ) {
        QVariant item;
        item << target[ i ];
        list.append( item );
    }
    data = list;
}

template<typename T>
void operator >>( const QVariant& data, QList<T>& target )
{
    QVariantList list = data.toList();
    target.reserve( list.count() );
    for ( int i = 0; i < list.count(); i++ ) {
        T item;
        list[ i ] >> item;
        target.append( item );
    }
}

This way any QList<T> can be converted from/to a QVariant as long as T can be converted from/to a QVariant. Note that we may also want to create additional specializations for QStringList and QVariantList, so that they are not unnecessarily converted, and to add similar conversion functions for maps and other containers.

To summarize, the following conversions are used before data is serialized:

  • Primitive types (numbers, strings and many other built-in types in Qt) are stored as QVariant
  • Objects are stored as QVariantMap that maps properties to values
  • List of various types are stored as QVariantList

The actual serialization consists of two steps: converting the serialized object into a QVariant and serializing the converted data into the stream. Deserialization is analogous and works in the opposite way.

You can notice that the converted data is somewhat similar to the DOM tree of XML document. A variant of a primitive type is analogous to a leaf XML node, and a map of variants is similar to an XML element with child nodes. However, this approach is more compact, faster and easier to use than XML.

Note that simple custom types don't necessarily have to be stored as a QVariantMap. For example, in a financial application, there may be a Money class, which is really a wrapper over some simple numeric value. We can directly place this numeric value in a variant (e.g. as a qlonglong) without wrapping it in a map.

We can also combine the method of serialization based on QVariantMap with the traditional approach, as described in the previous article, for certain types that highly unlikely to change, and can be treated as primitive types. For example, in a graphic application we might define a Circle class which consists of a central QPoint and a radius. We can use the Q_DECLARE_METATYPE macro and the qRegisterMetaTypeStreamOperators function, so that the Circle object can be directly wrapped into a variant and serialized without any conversions.

Just remember that when the Circle type is introduced in a later version of the application, previous versions will not be able to load a file that contains it, so we must remeber about the version tag, as described in the previous post. Also the version of the data format used by built-in Qt types is important to maintain compatibility across different environments.

Filed under: Blog

Serialization in Qt - part 2

In the previous post I wrote about support for different file formats in Qt and the pros and cons of using QDataStreama and a binary format. As I promised, today I will provide some more code. I will also start discussing various issues related to backward and forward compatibility of data files.

For now I will focus on simple cases like storing application settings or simple data like a list of bookmarks. I'm assuming that all serialized data have value-type semantics; i.e. they are stored and copied by value, not by pointer. A lot of classes in Qt are value types, including strings and all kinds of containers (as long as they store value types, not pointers). Also Qt makes it easy to create complex and efficient value types by using QSharedData and the copy on write mechanism, but that's an entirely different story.

In our example I will use the following simple Bookmark class:

class Bookmark
{
public:
    Bookmark();
    Bookmark( const Bookmark& other );
    ~Bookmark();

    const QString& name() const { return m_name; }
    // ... other getters/setters

    Bookmark& operator =( const Bookmark& other );

    friend QDataStream& operator <<( QDataStream& stream, const Bookmark& bookmark );
    friend QDataStream& operator >>( QDataStream& stream, Bookmark& bookmark );

private:
    QString m_name;
    QUrl m_url;
};

There is a default constructor, copy constructor and assignment operator; all that's needed for a value type. Thanks to this, we can store our objects in a container like QList. The two overloaded shift operators provide support for serialization. There's even no need to use Q_DECLARE_METATYPE, unless we need to put the bookmark in a QVariant or use it with asynchronous signal/slot connections.

The implementation of the shift operators is straightforward:

QDataStream& operator <<( QDataStream& stream, const Bookmark& bookmark )
{
    return stream << bookmark.m_name << bookmark.m_url;
}

QDataStream& operator >>( QDataStream& stream, Bookmark& bookmark )
{
    return stream >> bookmark.m_name >> bookmark.m_url;
}

This is serialization in its true sense: the bytes of the name string are directly followed by the bytes of the URL in the data stream. Without knowing the exact sequence of data, it's not possible to determine if the next byte is part of a string or an integer, and whether the string is part of the bookmark or some other structure. This means that extra care must be taken to read the data in exactly the same order as it was written.

While this is certainly efficient and makes the code extremely simple, there is a big problem when something needs to be changed or added. Let's suppose that a newer version of our application needs to support hierarchical bookmarks. We add a QList<Bookmark> m_children member to the Bookmark class and modify the implementation of the operators:

QDataStream& operator <<( QDataStream& stream, const Bookmark& bookmark )
{
    return stream << bookmark.m_name << bookmark.m_url << bookmark.m_children;
}

QDataStream& operator >>( QDataStream& stream, Bookmark& bookmark )
{
    return stream >> bookmark.m_name >> bookmark.m_url >> bookmark.m_children;
}

The QList automatically takes care of serializing all the items it contains by recursively calling the shift operator, so it might appear that nothing else needs to be done. But what happens if the user upgrades the application from the older version, and the new version tries to read the bookmarks file created by that previous version? It will expect the list of child bookmarks just after the URL, but actually it's some entirely different, random data. Attempting to interpret it as something it's not will give unexpected results and might even result in a crash.

The solution is to include a version tag in the stream, so that we can conditionally skip some fields when reading a file created by an older version of our application. For example:

QDataStream& operator <<( QDataStream& stream, const Bookmark& bookmark )
{
    return stream << (quint8)2 << bookmark.m_name << bookmark.m_url << bookmark.m_children;
}

QDataStream& operator >>( QDataStream& stream, Bookmark& bookmark )
{
    quint8 version;
    stream >> version >> bookmark.m_name >> bookmark.m_url;
    if ( version >= 2 )
        stream >> bookmark.m_children;
    return stream;
}

Note, however, that this would only work if the first version of the application also included the version tag in the stream! Otherwise we would attempt to read the first byte of the string as the version, again leading to unexpected results and potential crash. The lesson from this excercise is to think about this up front and always plan for the change.

Using a version tag is a good and universal solution, but it only provides backward compatibility: we can use it to correctly read files created by an older version. What happens if our application attempts to read a file created by a newer version of itself? We cannot predict what changes will be made in the future, so there's not much we can do to handle such situation. We can just close the stream and perhaps throw an exception to prevent the application from crashing.

Forward compatibility usually doesn't matter when it comes to simple configuration files. But what if the file is actually an important document that we need to send to someone else, who might have a slightly older version of the application? One solution would be to use a different format, like XML, but forward compatibility can also be achieved when using the QDataStream. I will write more about it in the next post.

Usually it doesn't make sense to include the version tag with each object, but just once at the beginning of the file. It's also a good idea to write a random "magic" value in the file header, to ensure that the file is really what we think it is. I use a class similar to the following one in my own applications to handle all this automatically:

class DataSerializer
{
public:
    DataSerializer( const QString& path ) :
        m_file( path )
    {
    }

    ~DataSerializer()
    {
    }

    bool openForReading()
    {
        if ( !m_file.open( QIODevice::ReadOnly ) )
            return false;

        m_stream.setDevice( &m_file );
        m_stream.setVersion( QDataStream::Qt_4_6 );

        qint32 header;
        m_stream >> header;

        if ( header != MagicHeader )
            return false;

        qint32 version;
        m_stream >> version;

        if ( version < MinimumVersion || version > CurrentVersion )
            return false;

        m_dataVersion = version;

        return true;
    }

    bool openForWriting()
    {
        if ( !m_file.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
            return false;

        m_stream.setDevice( &m_file );
        m_stream.setVersion( QDataStream::Qt_4_6 );

        m_stream << (qint32)MagicHeader;
        m_stream << (qint32)CurrentVersion;

        m_dataVersion = CurrentVersion;

        return true;
    }

    QDataStream& stream() { return m_stream; }

    static int dataVersion() { return m_dataVersion; }

private:
    QFile m_file;
    QDataStream m_stream;

    static int m_dataVersion;

    static const int MagicHeader = 0xF517DA8D;

    static const int CurrentVersion = 1;
    static const int MinimumVersion = 1;
};

It's basically a wrapper over a file with an associated data stream. It ensures that both the magic header and the version are correct when opening the file for reading, and writes those values when opening it for writing. The CurrentVersion constant should be incremented every time something is added or changed in the serialization code of any class. The MinimumVersion constant allows us to skip support for some really old versions, especially when data format changed too much. The dataVersion static method makes it easy to check the actual version when reading data from the stream:

QDataStream& operator >>( QDataStream& stream, Bookmark& bookmark )
{
    stream >> bookmark.m_name >> bookmark.m_url;
    if ( DataSerializer::dataVersion() >= 2 )
        stream >> bookmark.m_children;
    return stream;
}

Note that the version is stored in a global variable, so this code is not thread safe or re-entrant, but so far it's been enough for me in all situations. More elaborate solutions may be created if necessary.

You can also notice that the DataSerializer explicitly sets the version of the data format to Qt_4_6. This is important, because Qt also has a similar versioning mechanism for it's own serialization format. This may cause problems when data is read and written using different versions of Qt libraries. Here we just enforce compatibility with the minimum supported version of Qt, in this case 4.6. Alternatively, we could store the Qt version in the header along with our internal version and verify both when opening the file.

In the next post I will write more about the forward compatibility problem and about serializing complex hierarchies of objects with cross references.

Filed under: Blog

Serialization in Qt - part 1

Almost all applications need to store some data and be able to read it later, whether it's a document file or just some application settings. The data can be anything from a few integers to a complex hierarchy of objects. Although the Qt framework doesn't have a built-in serialization support in the same sense as, for example, .NET or Java, it provides at least three mechanisms that can make storing and reading data easier:

  • QSettings - the standard Qt way of storing application settings. It supports both a variation of INI file format and platform specific storage, e.g Windows Registry.
  • QDomDocument - along with other classes from the QtXml module, it provides support for XML files.
  • QDataStream - can be used to read and write binary files.

Each solution has it's advantages and disadvantages. The XML format is sometimes considered as the only "right" way to store any kind of data. While it certainly has many advantages, the markup adds a lot of overhead, and being text based, it's not very suitable for storing data that is binary in it's nature. The INI format is perhaps more compact, but it's still text based and (arguably) human readable. Although it is possible to store anything that can be wrapped in a QVariant, for example a QImage, reading and writing such data is not very efficient (it has to be serialized in binary format and then converted to escaped textual representation). Also such INI file is no longer human readable, not to mention editable. That makes the benefit of using a INI file over a plain binary file questionable.

Personally I use QSettings only in two situations:

  • For manipulating Registry settings in a more comfortable way than by directly using the Windows API (for example to register a custom file extension).
  • For reading auxiliary configuration files that are rarely changed, but can be altered by the user in certain situations. For example, I store the list of available languages in an INI file. Because the list is not hard-coded, new translations can be created or installed without having to recompile the whole application.

Support for XML files is nice if we need to handle one of the numerous existing file formats which is based on XML, for example SVG, RSS or OpenDocument. However I personally don't see much point for a new, custom file format to be based on XML. Unless it needs to be embedded or mixed with other XML based file formats, or processed with a XSLT processor, using a binary format is usually a better idea. Sometimes XML based formats are seen as more "open", whatever that means, but from a technical point of view that's compeletely irrelevant. There are numerous examples of open, well documented binary formats.

A more reasonable argument is that XML based formats are more flexible, because new attributes and tags can be added without affecting compatibility with older and newer versions of the software. With some additional effort, this can also be achieved when using a binary format. I will write more about this topic in one of the next posts.

Another concern is the binary compatibility of data on various platform. QDataStream nicely takes care of it by ensuring proper endianness. We just have to use types like qint32 instead of the standard C++ types when reading from/writing to the stream, to ensure that data always has the same size. On the other hand, in case of XML it would be necessary to take ensure that numeric precision is not lost when converting values to/from text.

The advantage of binary serialization is that it's very simple, fast and memory efficient. There is no addional overhead of parsing the XML markup, storing the entire DOM tree in memory, etc. It also requires much less code that needs to be written. Manipulating the DOM tree is cumbersome and not very elegant, and using the more efficient SAX-style interface is even more difficult.

In the simplest case, the application settings can be represented by a single QVariantMap object (equivalent to QMap<QString, QVariant>). This is basically the same as what QSettings provides, except that the latter uses additional prefixes to emulate a hierarchy of groups. Note that almost anything can be a variant, including custom types, and even another QVariantMap. This makes it easy to create complex, nested data structures that can be saved and loaded back using a few lines of code:

QVariantMap settings;

MyClass instance;
settings.insert( "Key", QVariant::fromValue( instance ) );

QFile file( "settings.dat" );
file.open( QIODevice::ReadOnly );

QDataStream stream( &file );
stream << settings;

In order for a custom type to be serializable, it only has to implement the << and >> operators taking the data stream object. In addition, to be able to embed the custom type in a QVariant, it must be declared as a metatype using the Q_DECLARE_METATYPE macro and registered using the qRegisterMetaTypeStreamOperators function. I will post an example in the next article.

When reading settings back, it's important to remember about default values. Although defaults can be used when reading the values, it's often better to initialize default values which are missing from the map at startup, just after reading the configuration file. This way the default value is only provided once, and not everywhere it's used.

Note that we don't always have to use QVariant to serialize data. If we want to have a file which stores just a list of bookmarks, we can simply serialize a QList<Bookmark>. All we need is the pair of << and >> operators. There is no need to declare a metatype; the type is static, so it doesn't have to be dynamically resolved upon deserialization. Also note that the Bookmark could even contain a nested list of child bookmarks.

Filed under: Blog

Impressions from Minecraft

Before I get to the point, just a few updates. I just finished refreshing the components available on this website, so now they're finally all up to date. I also published some recent photos of Adam. He's growing and changing so quickly that it's hard to keep up :). Last but not least, I released version 1.0.2 of WebIssues some time ago with some minor fixes and improvements.

Why do I write about Minecraft? It's simply impossible to avoid it. I had a short adventure with the free Classic version in January, when Adam was still at the hospital. I knew I shouldn't even think about the full version, because I'd be stuck with it for a long time. I managed to forget about it until recently I bought May Payne III and I was looking for an online review. Then I accidentally found that an Xbox version of Minecraft is available. I didn't even finish playing Max Payne (which is great, by the way, almost as much as the original version was 12 years ago) and bought Minecraft. It's not hard to guess that it costed me a few nights with hardly any sleep.

I think that there are already a few dissertations about the Minecraft phenomena. A world made of blocks that you can freely dig and move around is simply every geek's dream. Games like Max Payne have great graphics and action, but it's sometimes annoying that you can't just get off the linear path and venture into the Sao Paulo favelas. Games like GTA don't solve the problem - you can go anywhere you want, but there's not much interaction with the world other than shooting people and running them over. In Minecraft there are no limits. You are a god in a digital world. Add to it a few zombies and retro style graphics and you have a recipe for success.

The Xbox version of Minecraft is still quite bit behind the PC version. It lacks enchanting, potions, a lot of biomes (there is only forest and desert, at least in my world) and structures like villages and strongholds. Hopefully they will add these features soon, but even as it is now, it's a lot of fun to play. The size of the world is limited to 1024x1024 blocks, but it's more than enough for a single player or even a few players. It took me quite a while to travel around the entire world using a boat. Also there is still a lot of bugs. It happened to me once that after saving the game deep underground and loading it again, I was moved to the surface, surrounded by three creepers. It was quite annoying :).

I could just as well buy the PC version, but I like to separate playing from working, so I prefer not to have any games on my laptop :). Not to mention the effect of having a big screen and surround sound. Mouse and keyboard is probably more comfortable for this type of game, but you can get used to the pad. At the moment I'm a little bit burnt out, and I can definitely use a break. Fortunately in a few days we're going to Germany to visit my wife's sister. Besides the Euro Championship is more important at the moment :). But Minecraft is definitely the kind of game that you want to keep returning to, especially when new updates will be released. Keep up the good work, Mojang!

Filed under: Blog
Tags: personal
Syndicate content