QList changes in Qt 6

With Qt 6, changes are coming to many components. Containers are no exception. In this blog post I tried to capture the most significant changes to QList and related classes.

QVector and QList are unified

Previously, Qt featured very different implementations for these two containers: QVector being a natural and straightforward array-like container and QList being rather special in its implementation to nicely accommodate types defined and used by Qt itself. With Qt 6 updates to existing types, backed by what was done already in previous releases, it seemed that there is little to no benefit to have an implementation difference between the classes. Additionally, QVector proved to be a better choice in many cases.

Thus, in Qt 6, QVector and QList are unified and the model of QVector is used as the underlying implementation. What this means for more advanced users of the framework is that Qt 5 QList's extra level of indirection for generic types is now gone and elements are always directly stored in the allocated memory.

However, we decided that QList should be the real class, with implementation, while QVector should just become an alias to QList. The point of this is to:

  • simplify porting efforts as many Qt APIs work with QList rather than QVector and we believe so does the user code, at least at the level of interaction with Qt
  • make clear that QVector2D and friends are unrelated to QVector
  • be in sync with QStringList and QByteArrayList naming convention

Fast prepend

While basing the new implementation of QList on QVector, we recognised one important use case: prior to Qt 6, QList had an amortised constant time of insertion at the beginning (a.k.a. prepend). To address this property, QList in Qt 6 supports optimised prepend as well and, to have more aligned container implementations, so do QString and QByteArray.

Unfortunately, everything comes at a price. In this case the price is iterator validity across container-modifying operations. Unlike QVector in Qt 5, QList (and hence also QVector) in Qt 6 allows less freedom in keeping the iterators valid between operations. As a rule of thumb, consider that any operation, that adds elements to, or removes them from, the container, invalidates the "remembered" iterators, even when the QList is not implicitly shared. More details and per-function exceptions to this rule can be found in the Qt 6 documentation snapshots, with the documentation constantly being updated to better reflect the current state of things.

QList may shrink on elements removal

Normally, QList manages its memory automatically. When growing, it allocates excess memory in advance to address further growing. Symmetrically, we wanted QList to reduce the occupied space when it is decreasing in size, especially when element removals leave large amounts of unused space. Shrinking to smaller capacity gives lower memory footprint, which is a nice property in certain use cases. This ability was a feature request already since Qt 5, however, for compatibility reasons, we could not update the code before as our users relied on the existing behaviour.

Convenient by default, such memory management mechanism can also become inefficient, for example, a long enough sequence of additions leads to repeated reallocation and extra element copying. Likewise, recurrent removals cause a lot of shrinking. In such cases, additions and removals incur costs that may be avoided.

QList::reserve() is a method that can be utilised to reduce the frequency of reallocations and hint the QList to preserve existing memory capacity as long as possible. When adding elements to the QList, reserving (and thus allocating) a known size upfront makes subsequent calls to growing functions (e.g. prepend, append, insert) boil down to the copying of the new data only. Similarly, a call to reserve() (e.g. with the current size as an input argument) will make the following removals avoid reducing the capacity.

In a way, reserve() allows to interfere into QList's memory management. To restore automatic behaviour, you should call QList::squeeze(). This will both tailor the allocated space exactly to the number of the elements stored and hint the QList that you no longer require current memory capacity to be something to hold on to. Note, however, that if your data is short-lived, calling squeeze(), which may reallocate the memory, can be costly. It may be easier (and faster) to just wait for the QList to be destroyed.

QList's memory is not limited by 2GiB

Before Qt 6, QList was limited to use at most 2GiB of memory. With a strong dominance of 64-bit architectures already today, this is a needless obstacle for users who wish to use more space for their task.

In Qt 6, the underlying size type of QList is changed to address this and it is possible to create QLists that allocate bigger amounts of memory (within the limits of what the system provides, of course). Consequently, all QList methods are changed as well to align with this and now use qsizetype, instead of int.

As a downside, users would most likely see their compiler warn about narrowing conversions. Consider the following example.

Perfectly correct code in Qt5:

void myFunc(QList<MyType> data) {
    int size = data.size();
    // ...
}

... would get a compiler warning in Qt 6 due to the (narrowing) conversion from qsizetype to int. A natural solution is to use the auto keyword instead of a specific type. This is also a remedy when you want to build against both Qt 5 and Qt 6.

Other smaller changes

We have also simplified QStringList and it is now an alias to QList<QString> as opposed to the Qt 5 version, where QStringList was a distinct class derived from QList<QString>. Careful readers may notice that QStringList used to have special methods not available in QList, for instance, QStringList::join(). These QStringList-specific methods are still available but are baked into QList<QString> similarly to how it is done for QByteArrayList.

 

As you may know, the QList implementation takes advantage of the additional traits of its element type, provided by a use of the Q_DECLARE_TYPEINFO() macro. Certain type traits allow to use faster algorithms to improve "out-of-the-box" performance. We have simplified this mechanism:

  • Q_MOVABLE_TYPE and Q_RELOCATABLE_TYPE now mean the same thing. Since C++11 introduced a meaning for "movable" that is not quite exactly what Q_MOVABLE_TYPE meant (and means), we encourage client code to use Q_RELOCATABLE_TYPE instead
  • trivially copyable and trivially destructible types no longer need to be marked as relocatable. They will be detected as such automatically

In any case, when in doubt about a type, you can always check and ensure specific traits at compile time. For example:

static_assert(QTypeInfo<MyType>::isRelocatable);  // makes sure MyType is relocatable

You can read more on the subject in our documentation snapshots.

 

Do not hesitate to share your opinion, comment or suggest an improvement. Your feedback is what makes Qt better!

 


Blog Topics:

Comments