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 int
s 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:
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
.
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:
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
:
Q_DECLARE_METATYPE(ElementId)
qRegisterMetaType<ElementId>();
This allows us to use the new class like:
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:
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:
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);
}
qRegisterMetaTypeStreamOperators<ElementId>("ElementId");
Supporting QList, QSet or QHash
Using the new type in a QList
will behave as working with int
s, no surprises here. However, the Q_DECLARE_TYPEINFO
can be used to help the Qt
's generic containers to choose the appropriate storage methods:
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.
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.
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