Click here to Skip to main content
15,880,392 members
Articles / Desktop Programming / QT
Tip/Trick

Strongly Typed Identifiers with Qt

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
20 Feb 2019LGPL33 min read 3K   39   2  
Strongly Typeded Identifiers in Qt

Introduction

The idea for this article came after completing a refactoring to an eight year old project. As I tried to better understand the C++/Qt codebase to accommodate a new feature, I decided to replace the int used to identify an Element with an object, ElementId.

You will find this concept under the name of "primitive obsession", we are using an int to represent an identifier, making it difficult to differentiate between real ints and identifiers.

In this article, we will see a possible implementation using the Qt Framework.

Background

You should know C++ and have some knowledge of the Qt Framework.

Setting Up the Basics

Let's have a look at the Element class:

C++
class Element
{
public:
    int id() const
        { return m_id; }

private:
    int m_id;
};

The first step was to add a new class providing a thin wrapper over the int value, named ElementId.

C++
class ElementId
{
public:
    explicit ElementId(int value)
        : m_value(value)
        {}
    
    int value() const;
    
private:
    int m_value;
};

After that, I just let the compiler find the errors. The amount of errors can be discouraging, it's not unusual to get a few hundreds. After sorting the errors by type, I had a look at each one. The first problem I found was that the type wasn't always an int, sometimes it will be an uint or an int32.

Validation

The second problem was to agree on what a null or valid means. The validation was made in different ways:

if (id)
-or-
if (id != 0)
-or
if (id > 0)

So let's make one definition for a valid identifier and add it to our class:

C++
class ElementId
{
public:
    ElementId()
        : m_value(0)
        {}
    
    bool isNull() const
        { return m_value == 0; }
        
    bool operator!() const
        { return isNull(); }
        
    operator bool() const
        { return !isNull(); }
};

The two operators are not necessary, but if we have them, we can write statements like:

if (id) // the id is not null
-or-
if (!id) // the id is null

Other required operators will be the ones for comparison and assignment. Since they are not special, I will not show them here, but you can check the attached project for the complete definition.

Supporting QVariant

Sometimes, the identifiers will be stored as QVariant data in a QTreeViewItem or a QComboBox. If the identifiers need to be stored in a database, the Qt classes will also use the QVariant type. The first step is to make our type known to QVariant:

C++
Q_DECLARE_METATYPE(ElementId)

// register the type (typically in main.cpp)
qRegisterMetaType<ElementId>();

This allows us to use the new class like:

C++
ElementId e(10);
QVariant v = QVariant::fromValue(e);
ElementId e2 = v.value<ElementId>();
Q_ASSERT(e == e2);

Of course, one can also add methods like toVariant() and fromVariant().

Another nice thing about having a QVariant is that we can also use the QVariant's methods like toString() or toInt(). We let Qt know how the conversion should be done:

C++
class ElementId
{
public:
    QString toString() const
        { return QString::number(m_value); }
};

QMetaType::registerConverter<ElementId, QString>(&ElementId::toString);
QMetaType::registerConverter<ElementId, int>(&ElementId::value);

To complete our support for QVariant, we can also add stream operators:

C++
QDataStream& operator<<(QDataStream& out, const ElementId& id)
{
    out << (qint32) id.value();
    return out;
}

QDataStream& operator>>(QDataStream& in, ElementId& id)
{
    qint32 value;
    in >> value;
    id = ElementId(value);
}

// register the operators for QMetaType
qRegisterMetaTypeStreamOperators<ElementId>("ElementId");

Supporting QList, QSet or QHash

Using the new type in a QList will behave as working with ints, no surprises here. However, the Q_DECLARE_TYPEINFO can be used to help the Qt's generic containers to choose the appropriate storage methods:

C++
Q_DECLARE_TYPEINFO(ElementId, Q_PRIMITIVE_TYPE);

To use the new type as key in associative containers, we will need to provide a specialization for the hashing function. Since the only member of our class is an int, the existing implementation to hash an integer is used.

C++
int qHash(const ElementId& id) 
{ 
    return qHash(id.value()); 
}

Supporting QDebug

The streaming operator will check for a valid identifier, for a null identifier, "null" will be written.

C++
QDebug operator<<(QDebug debug, const ElementId& id)
{
    QDebugStateSaver saver(debug);
    debug.nospace();
    debug << "ProductId" << '(';
    if (productId.isNull())
        debug.noquote() << "null";
    else
        debug << productId.value();
    debug << ')';
    return debug;
}

Summary

We introduced a new value type to hold a product identifier, that provides basic validation and QVariant support.

History

  • 21st February, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer (Senior)
Austria Austria
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --