My question is basically when to choose QVector
and when to choose QList
as your Qt container. What I already know:
- Qt docs: QList class
For most purposes, QList is the right class to use. Its index-based API is more convenient than QLinkedList's iterator-based API, and it is usually faster than QVector because of the way it stores its items in memory. It also expands to less code in your executable.
The same is written is this very popular Q&A: QVector vs QList. It also favors QList.
But: on recent Qt World Summit 2015 KDAB presented "Why QList is harmful", this is basically here:
Don't use QList, use Q_DECLARE_TYPEINFO
As far as I understand the idea is that QList
for almost all types is inefficient when allocating new elements in heap. Each time you are adding new element, it calls new
(once per element) and this is inefficient compared to QVector
.
This is why now I am trying to understand: is it QVector
which we should choose as default container?
QList
is an array ofvoid*
.In its normal operation, it
new
s the elements on the heap and stores a pointer to them in thevoid*
array. Like a linked list, that means that references (but, unlike linked lists, not iterators!) to elements contained in the list remain valid under all container modifications until the element is removed from the container again. Thus the name "list". This datastructure is called an array-list and is used in a lot of programming languages where every object is of reference type (say, Java). It is a very cache-unfriendly data structure, like all node-based containers.But the resizing of the array-list can be factored into a type-independent helper class (
QListData
), which is supposed to save some executable code size. In my experiments, it's next to impossible to predict which ofQList
,QVector
orstd::vector
produces the least executable code.This would have been a good data type for the many Qt reference-like types such as
QString
,QByteArray
, etc., which consist of nothing more than a pimpl pointer. For these types,QList
gained an important optimisation: when the type is not larger than a pointer (and please note that this definition depends on the platform's pointer size - 32 or 64bits), instead of heap-allocating objects, the objects are stored in thevoid*
slots directly.This is only possible, though, if the type is trivially relocatable. That means it can be relocated in memory using
memcpy
. Relocation here means I take an object,memcpy
it to another address and - crucially - not run the destructor of the old object.And this is where things started to go wrong. Because unlike in Java, in C++ a reference to an object is its address. And while in the original
QList
, references were stable until the object was removed from the collection again, by putting them into thevoid*
array this property no longer holds. This is no longer a "list" for all intents and purposes.Things continued to go wrong, though, because they allowed types that are strictly smaller than a
void*
to be placed in aQList
, too. But the memory management code expects elements of pointer size, soQList
adds padding(!). That means that aQList<bool>
on 64bit platforms looks like this:Instead of fitting 64 bools into a cache line, like
QVector
does,QList
only manages 8.Things went wrong out of any proportion when the docs started calling
QList
a good default container. It's not. The original STL states:Scott Meyer's Effective STL has several items that start with "Prefer
std::vector
over...".What is true in general C++ is not suddenly wrong just because you're using Qt.
Qt 6 will fix that particular design mistake. In the meantime, use
QVector
orstd::vector
.QList behaves differently depending on what's inside (see source code
struct MemoryLayout
):if
sizeof T == sizeof void*
andT
is definedQ_MOVABLE_TYPE
, thenQList<T>
behaves exactly likeQVector
, that is, the data is stored contiguously in memory.if
sizeof T < sizeof void*
andT
is definedQ_MOVABLE_TYPE
, thenQList<T>
pads each entry tosizeof void*
, and loses layout-compatibility with QVector.in all other cases,
QList<T>
is a linked list and therefore slow to some degree.This behavior is what makes
QList<T>
pretty much always a bad choice, because depending on nifty details,QList<T>
is either really a list, or a vector. That's bad API design and prone to errors. (For instance, you will run into bugs if you have a library with a public interface that uses aQList<MyType>
internally and in its public interface.sizeof MyType is < sizeof void*
, but say you forgot to declare MyType asQ_MOVABLE_TYPE
. Later, you want to addQ_MOVABLE_TYPE
. This is binary incompatible, meaning that you now have to recompile all code that uses your library, as the memory layout ofQList<MyType>
changed in the public API. If you are not careful, you will miss this and introduce a bug. This illustrates quite nicely why QList is a bad choice here.)That said, QList is still not entirely bad: It will probably do what you want most of the cases, but maybe it will do the job behind the scenes differently to what you might expect.
Rule of thumb is:
Instead of QList, use
QVector<T>
orQVector<T*>
, since it explicitly says what you want. You can combine that withstd::unique_ptr
.In C++11 and onwards, it is even considered best to just use
std::vector
, since it will behave correctly in a range-based for loop. (QVector and QList may detach and therefore perform a deep-copy).You can find all these details and more in a presentation from Marc Mutz and in the video by Olivier Goffart.
Qt advertises
QList
as the "jack of all trades", but the other half of that saying is "master of none". I'd sayQList
is a good candidate if you plan on appending to both ends of the list, and those are no larger than than a pointer, asQList
reserves space before and after. That's about it, I mean as far as good reasons to useQList
are concerned.QList
will automatically store "large" objects as pointer and allocate the objects on the heap, which may be considered a good thing if you are a baby, which doesn't know how to declare aQVector<T*>
and use dynamic allocation. This is not necessarily a good thing, and in some cases it will only bloat the memory usage and add extra indirection. IMO it is always a good idea to be explicit about what you want, whether it is pointers or instances. Even if you do want heap allocation, it is always better to allocate it yourself and simply add the pointer to the list than construct the object once, then have have copy construct that on the heap.Qt will return you a
QList
in a lot of places where it comes with overhead, for example when getting aQObject
's children or you search for children. In this case it doesn't make sense to use a container that allocates space before the first element, as it is a list of objects which are already there, not something you are likely to prepend to. I also don't much like the absence of aresize()
method.Imagine a situation where you have an object with size of 9 bytes and byte alignment on a 64 bit system. It is "far too much" for
QList
so instead it will use 8 byte pointer + CPU overhead for the slow heap allocation + memory overhead for the heap allocation. It will use twice the memory and with an extra indirection for access it will hardly offer performance advantages as advertised.As of why
QVector
cannot suddenly become the "default" container - you don't change horses mid-race - it is a legacy thing, Qt being such an old framework, and even though a lot of stuff has been deprecated, making changes to widely used defaults is not always possible, not without breaking a lot of code, or producing undesired behavior. Good or bad,QList
will likely continue being the default all the way throughout Qt 5, and likely in the next major release as well. The same reason Qt will continue using "dumb" pointers, for years after smart pointers have become a must and everybody is crying about how bad plain pointers are and how they should not be used ever.That being said, nobody is forcing you to use
QList
in your design. There is no reason whyQVector
should not be your default container. I myself don't useQList
anywhere, and in the Qt functions which return aQList
I merely use as a temporary to move stuff into aQVector
.Furthermore, and this is only my personal opinion, but I do find a lot of design decisions in Qt that don't necessary make sense, be that performance or memory use efficiency or ease of use wise, and overall there are a a lot of frameworks and languages which like promoting their ways of doing things, not because it is the best way to do it, but because it is their way to do it.
Last but not least:
It really boils down to how you understand this. IMO in this context, "the right" does not stand for "the best" or "the optimal", but for "good enough" as in "it will do, even if not the best". Especially if you know nothing about different container classes and how they work.
To sum things up:
QList
PROsQVector
with explicit pointers to achieve the same and cheaper - no extra copy), since when resizing the list, no objects will be moved, only pointersQList
CONsresize()
method,reserve()
is a subtle trap, since it will not increase the valid list size, even if index access works it falls in the UB category, also you will not be able to iterate that listThe CON's marginally outweigh the PROs, meaning that while in "casual" use
QList
might be acceptable, you definitely don't want to use it in situations where CPU time and/or memory usage are a critical factor. All in all,QList
is best suited for lazy and careless use, when you don't want to make the consideration of optimal storage container for the use case, which would typically be aQVector<T>
, aQVector<T*>
or aQLinkedList
(and I exclude "STL" containers, since we are talking Qt here, Qt containers are just as portable, sometimes faster, and most certainly easier and cleaner to use, whereasstd
containers are needlessly verbose).In Qt 5.7, the documentation was changed concerning the topic discussed here. In QVector it is now stated:
They refer to this article by Marc Mutz.
So the official point of view has changed.
I'd tend to say the opposite. It'll be much worse off, when going through the items. If it stores it as pointers on the heap won't QList be much worse off than QVector? The reason that sequential storage(QVector all the time) is so good is, that is is cache friendly, once you store pointers,you lose the data locality, start getting cache misses and it's horrible for performance.
The "default" container IMHO should be a QVector (or std::vector), if you're worried about lots of reallocation, then preallocate a reasonable amount, pay the once off cost and you'll benefit in the long run.
Use the *Vector by default, if you get performance problems, profile and change as necessary.
QList is the best possible container to use generally as the documentation states. If the size of the elements' type is <= of the pointer's size = machine & OS bitness = 4 or 8 bytes then the objects are stored the same way as QVector does - sequentially in memory. If the size of the QList's element type is greater than the pointer's size QList performs better than QVector because it doesn't store the objects sequentially but stores sequentially pointers to heap copies. In the 32-bit case the picture is as follows:
In case you want your objects to be laid out sequentially in memory no matter the size of their elements, as it is usually the case with OpenGL programming, then you should use QVector.
Here is a detailed description of the QList's internals.