In this post, I want to show some examples how C++11’s initializer lists can improve some typical Qt code. I’ll show how C++11 makes the initialisation of container-type variables very simple and how it makes static const data members possible at all.
Example 1: Initialising Container-Type Variables
Initialising container-type variables in C++03 and older was cumbersome. The typical re-implementation of the function QAbstractItemModel::roleNames()
proves this point. Subclasses of QAbstractItemModel
must reimplement this function such that, say, a QML ListView
can use a C++ role as a QML property. The old way looked similar to this.
// Code Snippet #1 (Old C++) QHash<int32, QByteArray> MyModel::roleNames() const { static QHash<int32, QByteArray> roles; if (roles.isEmpty()) { roles[ROLE_LAST_NAME] = "lastName"; roles[ROLE_FIRST_NAME] = "firstName"; } return roles; }
The first time roleNames()
is called, the static variable roles
is empty and it is initialised with the two name-value pairs. The initialisation is skipped in further calls.
The way C++11 way of writing this function looks as follows.
// Code Snippet #2 (C++11) QHash<int32, QByteArray> MyModel::roleNames() const { return QHash<int32, QByteArray>{ { ROLE_LAST_NAME, "lastName" }, { ROLE_FIRST_NAME, "firstName" } }; }
As Simon pointed out in the comments, this version is less efficient than the original version. The hash map with the role names is created every time the function roleNames
is called. This is easy to fix. We can declare a static const variable roles
as in the original example, initialise it with the hash map, and return roles
.
// Code Snippet #2b (C++11) QHash<int32, QByteArray> MyModel::roleNames() const { static const auto roles = QHash<int32, QByteArray>{ { ROLE_LAST_NAME, "lastName" }, { ROLE_FIRST_NAME, "firstName" } }; return roles; }
Now, the static variable roles
is created only once for all instances of MyModel
. Thanks to C++11’s auto
and initializer list, the function roleNames()
also becomes simpler to read.
The solution for MyModel::roleNames()
above can be used in general to initialise container-type variables. The classic example would be initialising a data member of type QStringList
. Before C++11, we would have done it as follows.
// Code Snippet #3 (Old C++) // File: decoder.h class Decoder { private: QStringList m_combinedChars; } // File: decoder.cpp Decoder::Decoder() { m_combinedChars.append(", "); // Or: push_back() instead of append() m_combinedChars.append("? "); m_combinedChars.append("! "); // ... }
In C++11, the constructor becomes much simpler.
// Code Snippet #4 (C++11) // File: decoder.cpp Decoder::Decoder() : m_combinedChars{ ", ", "? ", "! " } { // ... }
Example 2: Static Constant Data Members of Container Types
We are going to do something now that pretty cumbersome in the pre-C++11 days (see Patrick’s comment below how to do it in the olden days). We’ll declare a container-type variable constant and initialise it in the constructor.
// Code Snippet #5 (C++11) // File: decoder.h class Decoder { private: const QStringList m_combinedChars; } // File: decoder.cpp Decoder::Decoder() : m_combinedChars{ ", ", "? ", "! ", "; ", ": ", " (", ") " } { // ... }
As m_combinedChars
is the same constant for all instances of Decoder
, we could declare it as static
as well.
// Code Snippet #6 (C++11) // File: decoder.h class Decoder { private: static const QStringList m_combinedChars; } // File: decoder.cpp const QStringList Decoder::m_combinedChars = QStringList{ ", ", "? ", "! ", "; ", ": ", " (", ") " } Decoder::Decoder() { /* ... */ }
This is a lot better than with C++03 and before, where we were only able to declare data members of built-in types as static const
.
My first thought with the above code was that I should be able to simplify it further using C++11’s auto
.
// Code Snippet #7 (C++11) // File: decoder.cpp const auto Decoder::m_combinedChars = QStringList{ ", ", "? ", "! ", "; ", ": ", " (", ") " } // Compiler error: conflicting declaration 'const auto Decoder::m_combinedChars'
The above code is perfectly fine, as we took care that the declaration in the header file and the initialisation in the source file yield the same type for m_combinedChars
. However, it is pretty easy to screw this up.
// Code Snippet #8 (C++11) // File: decoder.h class Decoder { private: static const double hugeNumber; // Type is double! } // File: decoder.cpp const auto Decoder::hugeNumber = 5.3f; // Type is float!
We have a real type conflict in this example. hugeNumber
has type double
in the header file and type float
in the source file. With auto
, the compiler never tries implicit conversions. Why should the compiler make an exception here.
The real problem is the code duplication. The compiler forces us to declare static data members in the header and initialise them in the source file. We should be allowed to initialise the data member in the header file and get rid of the initialisation in the source file. And indeed, C++11 allows this.
// Code Snippet #9 (C++11) // File: decoder.h class Decoder { private: static constexpr auto hugeNumber1 = 5.3; // Type is double! static const double hugeNumber2 = 5.3; // Compiler error! }
We must use constexpr
instead of const
. Initialising a static const data member in the class declaration is still only allowed for integral and enumeration types. g++ does not fail on the second declaration above in older version, but this has always been non-standard.
Ideally, we would like to declare and initialise m_combinedChars
in the same way as hugeNumber1
.
// Code Snippet #10 (C++11) // File: decoder.h class Decoder { private: static constexpr auto hugeNumber1 = QStringList{ ", ", "? ", "! ", "; ", ": ", " (", ") " }; // Compiler error: field initializer is not constant }
Finally, we are out of luck. This is not possible in C++11! The compiler cannot figure out that the initialisation expression is constant. The constructors of the class QStringList
are too complex.
The above code works fine for trivial classes whose constructors could be generated by the compiler. The class QPoint
, for example, is such a trivial class.
// Code Snippet #11 (C++11) // File: decoder.h class Decoder { private: static constexpr auto point = QPoint{3, 7}; } // File: qpoint.h // Kind of constructors considered trivial! inline QPoint::QPoint() : xp(0), yp(0) {} inline QPoint::QPoint(int xpos, int ypos) : xp(xpos), yp(ypos) {}
Nice article!
> This is a lot better than with C++03 and before, where
> we were only able to declare data members of built-in
> types as static const.
Actually it was possible to have a static, constant container type variable as a class member using a helper function to do the initialization. For example…
In the class declaration:
In the implementation:
Not that bad compared to the new C++11 way which is even better of course.
Thanks for pointing this out! So, it’s possible to use static const variables in old C++, but it’s pretty cumbersome compared to C++11.
It is most unfortunate how the role names example is worse with initializer syntax. Every time the function is called a new QHash is allocated and populated (including hashing, etc.). The initializer list syntax is merely syntactic sugar, it does not avoid the cost of hashing and allocating memory. The version with the function static variable is superior in terms of performance (hash only once populated) and Memory consumption (implicitly shared copy).
Good point! Then, let’s combine the initializer list with the static variable to enjoy both worlds: simple syntax and best performance.
Yeah, that’s nice 🙂