Chapter 4. The Container Library

Are you animal, vegetable, or mineral?
— Lewis Carroll, Through the Looking-Glass

One of the most common tasks in programming is managing groups of elements. Implementing and debugging programs that use such data structures takes up a significant amount of developer time, since the same problems must be solved over and over again.

In this chapter we will explore the Qt6 container library. Containers are template classes designed to store elements of a single type. They provide type-safe and easy-to-use alternatives to plain C arrays. The Qt container library discussed here is called Tulip.

A container is an object designed to store and manage the elements it holds. It handles memory allocation and deallocation, as well as element insertion and removal. A container hides memory management details from the developer and provides a convenient interface for working with data.

The primary purpose of containers is to free developers from manually managing memory allocation and deallocation, and to supply efficient algorithms for working with data. This allows developers to focus on building the application itself rather than on the implementation details of the container classes they use.

The Tulip library is an integral part of Qt and is actively used throughout the framework. It closely resembles the STL (Standard Template Library) and is compatible with it. Developers are free to choose between Tulip and STL. An additional argument in favor of Tulip is that the library has been optimized for performance and memory efficiency in accordance with Qt’s design principles. It is worth noting that the use of template classes on which containers are built can noticeably increase the size of compiled binaries. This is because each object file that uses containers contains compiler-generated container code for the specific types used, and that code may be duplicated. The Tulip library was designed specifically with this in mind and has been optimized to significantly reduce the size of the generated object code.

The Tulip class implementations reside in the QtCore module. At the core of the Tulip library (just as in STL) are three concepts:

  1. container classes (containers);
  2. algorithms;
  3. iterators.

Their interrelationship is illustrated in Fig. 4.1.

Diagram showing how Qt6 containers, iterators, and algorithms relate to each other
Fig. 4.1. The relationship between containers, iterators, and algorithms

Container Classes

Container classes are classes that can store elements of various data types. Almost all container classes in Qt are implemented as templates and can therefore store data of any type. The core idea of a template is to create a generic class whose type is specified when an object of that class is instantiated. Container classes can hold entire collections of other objects, which may themselves be containers.

Choosing the right container for a given situation requires a clear understanding of the differences between container types. This choice significantly affects both execution speed and memory efficiency. Qt provides two categories of container classes: sequential containers and associative containers.

Sequential containers are ordered collections where each element occupies a specific position. An element’s position depends on where it was inserted. Sequential containers include: list, stack, and queue. Qt provides three classes in this category:

  1. QList<T> — list;
  2. QStack<T> — stack;
  3. QQueue<T> — queue.

Associative containers are collections in which an element’s position depends on its value — that is, once elements are inserted, their order is determined by their values. Associative containers include: set, map, and hash. Classes in this category:

  1. QSet<T> — set;
  2. QMap<K,T> — map;
  3. QMultiMap<K,T> — multi-map;
  4. QHash<K,T> — hash;
  5. QMultiHash<K,T> — multi-hash.

All containers in both groups support the operations listed in Table 4.1. Please note a few exceptions for QSet<T>.

Table 4.1. Operators and methods defined in all container classes

Operator / Method Description
== and != Comparison operators: equal and not equal
= Assignment operator
[] Subscript operator. The only exception is QSet<T> — this operator is not defined for it
begin() and constBegin() Methods that return iterators positioned at the beginning of the container’s element sequence. For QSet<T>, only constant iterators are returned
end() and constEnd() Methods that return constant iterators positioned past the end of the container’s element sequence
clear() Removes all elements from the container
insert() Inserts an element into the container
remove() Removes an element from the container
size() and count() Both methods are identical — they return the number of elements in the container, but size() is preferred as it is consistent with STL
value() Returns the value of a container element. Not defined in QSet<T>
empty() and isEmpty() Return true if the container contains no elements. Both methods are identical, but empty() is preferred as it is consistent with STL

Iterators

At some point you will need to traverse the elements of a container. Iterators are designed for exactly this purpose. Iterators abstract away the underlying data structure of a container — if you decide at some point that a different container type would be more efficient, all you need to do is swap in the new type. The rest of the code that uses iterators will not be affected.

Qt provides two iterator styles:

  1. Java-style iterators;
  2. STL-style iterators.

As a modern approach, it is recommended to use standard loops and the range-based for loop (available in C++11 and later).

Java-Style Iterators

Java-style iterators are very easy to use. They were designed specifically for developers who have no prior experience with STL containers. Their key distinction from STL-style iterators is that they do not point to an element itself, but rather to the position between two adjacent elements. At the start, the iterator points to the position before the first element of the container, and each call to next() (see Table 4.2) advances it by one position. However, Java-style iterators are objects, not raw pointers. In most cases their use results in more compact code than STL-style iterators:

QList<QString> list = {"Within Temptation", "Night Rune", "Mantus"};
QListIterator<QString> it(list);
while(it.hasNext()) {
    qDebug() << "Element:" << it.next();
}

Table 4.2 lists the methods of the QListIterator class, which also apply to the QHashIterator, QMapIterator, and QSetIterator classes. These iterators are constant, so modifying element values, inserting, or removing elements is not possible.

Store pointers to objects derived from QObject in containers

It is important to remember that classes derived from QObject do not have an accessible copy constructor or assignment operator, since these are declared in the private section. Consequently, their objects cannot be stored directly in containers — you must store pointers to QObject-derived objects rather than the objects themselves. In Qt6, it is recommended to use QPointer or std::shared_ptr for this purpose to enable automatic memory management.

Use the empty() method to check whether a container holds any elements

If you need to check whether a container is empty, use empty() rather than size(). The reason is simple: empty() is guaranteed to run in constant time for all containers, which can significantly improve the performance of your algorithm.

Table 4.2. Methods of QListIterator, QHashIterator, QMapIterator, QSetIterator

Method Description
toFront() Moves the iterator to the front of the list
toBack() Moves the iterator to the back of the list
hasNext() Returns true if the iterator is not at the end of the list
next() Returns the value of the next element and advances the iterator to the next position
peekNext() Returns the next value without changing the iterator’s position
hasPrevious() Returns true if the iterator is not at the beginning of the list
previous() Returns the value of the previous element and moves the iterator back one position
peekPrevious() Returns the previous value without changing the iterator’s position
findNext(const T&) Searches for a given element in the forward direction
findPrevious(const T&) Searches for a given element in the backward direction

If you need to modify elements while iterating, use mutable iterators. Their class names follow the same pattern with the addition of Mutable: QMutableListIterator, QMutableHashIterator, QMutableSetIterator, and QMutableMapIterator. The remove() method removes the current element, and insert() inserts an element at the current position. You can assign a new value to an element using setValue().

Let’s reassign the list element "Boney M" to "Rolling Stones":

QList<QString> list = {"Beatles", "ABBA", "Boney M"};
QMutableListIterator<QString> it(list);
while(it.hasNext()) {
    if (it.next() == "Boney M") {
        it.setValue("Rolling Stones");
    }
    qDebug() << it.peekPrevious();
}

The main drawback of Java-style iterators is that their use typically produces noticeably larger object code compared to STL-style iterators, which we will now examine.

STL-Style Iterators

STL-style iterators are somewhat more efficient than Java-style iterators and can be used together with STL algorithms. For C++ developers, these are arguably the most natural type of iterator. STL-style iterators can be thought of as generalized pointers that reference elements of a container.

Calling begin() on a container object returns an iterator pointing to its first element, while calling end() returns an iterator pointing past the end of the container — not to the last element, but to the position that would follow it. In other words, this iterator does not point to any element; it merely marks the end of the container (Fig. 4.2).

Diagram of an STL-style iterator showing the begin() and end() positions in a Qt container
Fig. 4.2. The begin(), end() methods and the current position

The ++ and \-- operators on an iterator advance it to the next or previous element, respectively. The element the iterator points to can be accessed using the dereference operator *. For example:

QList<QString> lst = {"In Extremo", "Blackmore's Night", "Night Rune"};
QList<QString>::iterator it = lst.begin();
for (; it != lst.end(); ++it) {
 qDebug() << "Element:" << *it;
}

The output will be:

Element: "In Extremo"
Element: "Blackmore's Night"
Element: "Night Rune"

Note that the loop uses the pre-increment operator ++it to advance the iterator. This avoids saving the old value on each iteration as a post-increment would, making the loop more efficient.

When iterating in reverse with the \-- operator, keep in mind that it is not symmetrical with forward iteration using ++. The reverse loop must therefore be written as follows:

QList<QString>::iterator it = lst.end();
for (;it != lst.begin();) {
    --it;
 qDebug() << "Element:" << *it;
}

The output will be:

Element: "Night Rune"
Element: "Blackmore's Night"
Element: "In Extremo"

If you only need to read element values without modifying them, it is much more efficient to use a constant iterator, const_iterator. In that case, use constBegin() and constEnd() instead of begin() and end(). Our example then becomes:

QList<QString> lst = {"In Extremo", "Blackmore's Night", "Night Rune"};
QList<QString>::const_iterator it = lst.constBegin();
for (; it != lst.constEnd(); ++it) {
 qDebug() << "Element:" << *it;
}

It is also worth noting that these iterators can be used with standard STL algorithms defined in the <algorithm> header. For example, to sort a list using the STL sort() algorithm:

QList<QString> lst = {"In Extremo", "Blackmore's Night", "Night Rune"};
std::sort(lst.begin(), lst.end());
qDebug() << lst;

The output will be:

QList("Blackmore's Night", "Cultus Ferox", "Night Rune")

Qt also provides its own algorithms, which we will cover later in this chapter.

Modern Iteration with Range-Based for

C++11 introduced the range-based for loop, which greatly simplifies iterating over containers. Qt6 fully supports this syntax for all its containers:

QList<QString> list = {"In Extremo", "Blackmore's Night", "Night Rune"};
for (const QString &str : list) {
    qDebug() << "Element:" << str;
}

This approach is preferred in modern C++ code, as it is more readable and less error-prone.

For cases where you need to modify container elements:

QList<QString> list = {"In Extremo", "Blackmore's Night", "Night Rune"};
for (QString &str : list) {
    str = str.toUpper();
}
qDebug() << list;
 

Using STL Algorithms with Lambdas

Modern C++ not only supports range-based for but also enables the use of standard STL algorithms such as std::for_each. This is especially convenient when you want to apply a function or lambda expression to every element of a container. For example, to print all elements of a list:

QList<QString> list = {"In Extremo", "Blackmore's Night", "Night Rune"};
std::for_each(list.begin(), list.end(), [](const QString &str){
    qDebug() << str;
});

This approach makes the code concise and declarative: instead of an explicit loop, all iteration logic and element processing is encapsulated within the standard algorithm. This improves readability, simplifies refactoring, and integrates well with other STL tools.

The foreach Keyword

Of course, the C++ language has no such keyword foreach — it was introduced artificially via the preprocessor and represents a loop variant designed to iterate over all elements of a container. This approach is an alternative to a constant iterator. For example:

QList<QString> list = {"Subway to sally", "Rammstein", "After Forever"};
foreach(QString str, list) {
    qDebug() << "Element:" << str;
}
Note!

The foreach macro is still supported in Qt6 but is considered deprecated. It is recommended to use the range-based for syntax instead, which is standard in C++11 and later.

In foreach, just as in regular loops, you can use break and continue, and loops can be nested.

Modifying element values inside a foreach loop does not affect the original container

Qt makes a copy of the container upon entering the foreach loop, so any changes to element values inside the loop will not affect the original container.

Sequential Containers

Sequential containers are ordered collections where each element occupies a specific position. The operations available for all sequential containers are listed in Table 4.3.

Table 4.3. Common methods of sequential containers

Operator / Method Description
+ Merges elements from two containers
+= Appends an element to the container (same as <<)
<< Appends an element to the container
at() Returns the specified element
back() and last() Return a reference to the last element. Both methods assume the container is non-empty. back() and last() are identical, but back() is preferred as it is consistent with STL
contains() Checks whether the element passed as a parameter is present in the container
erase() Removes the element at the iterator position passed as a parameter
front() and first() Return a reference to the first element of the container. Both methods assume the container is non-empty. front() and first() are identical, but front() is preferred as it is consistent with STL
indexOf() Returns the position of the first occurrence of a matching element in the container.
lastIndexOf() Returns the position of the last occurrence of a matching element in the container.
mid() Returns a container holding copies of elements defined by a starting position and a count
pop_back() Removes the last element of the container
pop_front() Removes the first element of the container
push_back() and append() Both methods append one element to the end of the container. They are identical, but push_back() is preferred as it is consistent with STL
push_front() and prepend() Both methods prepend one element to the beginning of the container. They are identical, but push_front() is preferred as it is consistent with STL
replace() Replaces the element at the specified position with the value passed as the second parameter

Example:

QList<QString> lst;
lst.append("In Extremo");
lst.append("Blackmore's Night");
lts.append("Night Rune");
qDebug() << lst;

or in a single line:

list << "In Extremo" << "Blackmore's Night" << "Night Rune";

The output will be:

QList("In Extremo", "Blackmore's Night", "Night Rune")

As shown in Table 4.3, the primary operations are element access, element insertion/removal, appending to the end, and prepending to the beginning.

Table 4.4 shows how quickly these operations are performed for each container. Use this table to select the container that will perform best for your specific use case.

Table 4.4. Operation performance for sequential containers

Container Access Insert / Remove Append to End Prepend to Front
QList Fast Slow Fast Fast
QQueue Fast Slow Fast Slow
QLinkedList Slow Fast Fast Fast

Byte Array: class QByteArray

In Qt6, the QByteArray class continues to serve as a container for storing a sequence of bytes (1-byte values) but is not a template class. Its main capabilities are described below.

Initialization with a Given Length

Objects of type QByteArray can be used wherever intermediate data storage is needed. The number of array elements can be specified in the constructor, and elements are accessed using the [] operator:

QByteArray arr(3, 0); // three zero bytes
arr[0] = arr[1] = 0xFF;
arr[2] = 0x2;

Compression and Decompression

Data in QByteArray objects can be compressed and decompressed. This is accomplished using two global functions: qCompress() and qUncompress(). Here we simply compress and then decompress the data:

QByteArray a           = "Test Data";
QByteArray aCompressed = qCompress(a);
qDebug() << qUncompress(aCompressed);

The output will be: "Test Data".

Base64 Encoding and Decoding

Sometimes you need to convert binary data to a text representation — for example, when embedding a bitmap image into an XML file (see Chapter 40). The QByteArray class provides two methods for this: toBase64() and fromBase64(). As their names suggest, the binary data is encoded in Base64 format, which was specifically designed for transmitting binary data as text. Here is a small example — we apply the conversion to a plain text string so the transformation is easy to follow:

QByteArray a       = "Test Data";
QByteArray aBase64 = a.toBase64();
qDebug() << aBase64;

The output will be: "VGVzdCBEYXRh".

Now let’s reverse the conversion using the static method fromBase64():

qDebug() << QByteArray::fromBase64(aBase64);

The output will be: "Test Data".

To minimize the space binary data occupies in a text file, you can compress it before encoding it in Base64.

Bit Array: class QBitArray

This class manages a bit (or boolean) array. Each stored value occupies only one bit, using no extra memory. Values are packed into bytes using QByteArray internally. This approach is used for storing large numbers of bool variables.

The QBitArray class provides testBit() for reading and setBit() for writing individual bits. In addition to these methods, the [] operator allows you to access each bit individually:

QBitArray bits(3);
bits[0] = bits[1] = true;
bits[2] = false;
 

Lists: QList<T>

A list is a data structure representing an ordered collection of linked elements. In general terms, this class represents an array of elements stored contiguously in memory. To check whether a list is empty, use the empty() or isEmpty() methods.

Avoid calling size() frequently to query the number of list elements, since each call triggers a count operation, which can noticeably impact performance. In cases where you simply need to know whether the list is empty, always use empty() or isEmpty() without hesitation.

Insertion and removal at arbitrary positions are very inefficient!

Avoid using removeAt() and insert() on QList<T>, since insertion and removal at arbitrary positions are very inefficient (see Table 4.4). For these operations, the QLinkedList class described later is a better choice.

Lists are implemented by the class QList<T>. In general terms, this class represents an array of pointers to elements (Fig. 4.3).

Internal structure diagram of the QList container class in Qt6
Fig. 4.3. List structure

The primary methods for lists are listed in Table 4.5.

Table 4.5. Selected methods of the QList<T> container

Method Description
move() Moves an element from one position to another
removeFirst() Removes the first element of the list
removeLast() Removes the last element of the list
swap() Swaps two elements at the specified positions
takeAt() Returns the element at the specified position and removes it
takeFirst() Removes and returns the first element
takeLast() Removes and returns the last element
toSet() Returns a QSet<T> container with the data from the QList<T> object
toStdList() Returns a standard STL std::list<T> with the elements from the QList<T> object
reserve() Reserves memory for a specified number of elements
resize() Resizes the list to the specified number of elements

If you do not intend to modify element values, avoid using the subscript operator [] for performance reasons. Use the at() method instead, since it returns a constant reference to the element.

One of the most common operations is traversing a list to sequentially read the value of each element. For example:

QList<int> list;
list << 10 << 20 << 30;
 
QList<int>::iterator it = list.begin();
while (it != list.end()) {
    qDebug() << "Element:" << *it;
 ++it;
}

The console output will be:

Element:10
Element:20
Element:30
 

Stack: class QStack<T>

A stack QStack<T> implements a data structure that follows the LIFO (Last In, First Out) principle — the element removed first is the one that was inserted most recently (Fig. 4.4).

Diagram illustrating the LIFO push and pop operation of a Qt6 QStack
Fig. 4.4. Stack operation principle

The QStack<T> class is an implementation of the stack data structure. It inherits from QList<T>. The process of adding elements to the stack is called pushing, and removing the top element is called popping. Each push increases the stack size by 1, and each pop decreases it by 1. The QStack<T> class defines push() and pop() methods for these operations. The top() method returns a reference to the element at the top of the stack. The following example demonstrates how to use the stack class:

QStack<QString> stk;
stk.push("Era");
stk.push("Night Rune");
stk.push("Gathering");
 
while (!stk.empty()) {
   qDebug() << "Element:" << stk.pop();
}

The console output will be:

Element:"Gathering"
Element:"Night Rune"
Element:”Era”
 

Queue: class QQueue<T>

A queue implements a data structure that follows the FIFO (First In, First Out) principle — the element removed first is the one that was inserted earliest (Fig. 4.5). The queue is implemented by the QQueue<T> class, which inherits from QList<T>.

Diagram illustrating the FIFO enqueue and dequeue operation of a Qt6 QQueue
Fig. 4.5. Queue operation principle

The following example demonstrates the use of a queue:

QQueue<QString> que;
que.enqueue("Era");
que.enqueue("Corvus Corax");
que.enqueue("Gathering");
 
while (!que.empty()) {
   qDebug() << "Element:" <<que.dequeue();
}

The output will be:

Element:"Era"
Element:"Corvus Corax"
Element:”Gathering"

Associative Containers

The purpose of associative containers is to store key-value associations. This allows elements to be accessed by key rather than by index. All containers of this type (with a few exceptions for QSet<T>) support the methods listed in Table 4.6.

Table 4.6. Common methods of associative containers

Method Description
contains() Returns true if the container holds an element with the specified key; otherwise returns false
erase() Removes an element from the container based on the iterator passed
find() Searches for an element by value. On success, returns an iterator pointing to that element; on failure, returns an iterator pointing to end()
insertMulti() Inserts a new element into the container. If an element with the same key already exists, an additional element is created. Not available in QSet<T>
insert() Inserts a new element into the container. If an element with the same key already exists, it is replaced. Not available in QSet<T>
key() Returns the first key corresponding to the value passed to this method. Not available in QSet<T>
keys() Returns a list of all keys in the container. Not available in QSet<T>
take() Removes the element with the specified key and returns a copy of its value. Not available in QSet<T>
unite() Adds elements from one container into another
values() Returns a list of all values in the container

Maps QMap<K,T> and QMultiMap<K,T>

In programming, a map is conceptually similar to a real-world dictionary. It stores elements of the same type indexed by key values. The key advantage of a map is that it allows you to quickly retrieve the value associated with a given key. Keys must be unique (Fig. 4.6), except in a multi-map, which allows duplicate keys (Fig. 4.7).

Diagram of a QMap showing unique key-value pairs in Qt6
Fig. 4.6. Map
Diagram of a QMultiMap showing multiple values stored under duplicate keys
Fig. 4.7. Multi-map

Elements are inserted into containers of this type along with the keys used to look them up; keys can be of any type. When using QMap<K,T>, you must ensure that no two different elements are inserted with the same key — otherwise one of them will be unreachable. That is, every key in a QMap<K,T> must be unique. Table 4.7 lists some of its methods.

Table 4.7. Selected methods of the QMap<K,T> container

Method Description
lowerBound() Returns an iterator pointing to the first element with the specified key
toStdMap() Returns a standard STL map containing the elements from the QMap<T> object
upperBound() Returns an iterator pointing past the last element with the specified key

One of the most common ways to access map elements is by using the key with the [] operator. However, you can also retrieve the key and value using the iterator’s key() and value() methods. For example:

QMap<QString, QString> mapPhonebook;
mapPhonebook["Piggy"]  = "+69 631322187";
mapPhonebook["Kermit"] = "+69 631322181";
mapPhonebook["Gonzo"]  = "+69 631322186";
 
QMap<QString, QString>::iterator it = mapPhonebook.begin();
for (;it != mapPhonebook.end(); ++it) {
    qDebug() << "Name:" << it.key()
             << " Phone:" << it.value();
}

The console output will be:

Name:Gonzo Phone:+69 631322186
Name:Kermit Phone:+69 631322181
Name:Piggy Phone:+69 631322187

Pay particular attention to the use of the [] operator, which can be used both to insert and to retrieve element values. However, be careful: if you specify a key for which no element exists, an element will be created. To avoid this, always check for the existence of an element associated with a key. You can do this using the contains() method. For example:

if(mapPhonebook.contains("Kermit")) {
    qDebug() << "Phone:" << mapPhonebook["Kermit"];
}

In practice, you may need to store multiple phone numbers for the same person — for example, their home, work, and mobile numbers. A regular QMap<K,T> is not suitable for this, and you will need to use the QMultiMap<K,T> multi-map. Using the example illustrated in Fig. 4.9, let’s add some code and retrieve the phone numbers for Piggy:

QMultiMap<QString, QString> mapPhonebook;
mapPhonebook.insert("Kermit", "+69 631322181");
mapPhonebook.insert("Gonzo", "+69 631322186");
mapPhonebook.insert("Gonzo", "+69 631322000");
mapPhonebook.insert("Gonzo", "+69 631322010");
mapPhonebook.insert("Piggy", "+69 631322187");
mapPhonebook.insert("Piggy", "+69 631322999");
 
QMultiMap<QString, QString>::iterator it =
                                mapPhonebook.find("Piggy");
for (; it != mapPhonebook.end() && it.key() == "Piggy"; ++it) {
    qDebug() << it.value() ;
}
 

Hashes QHash<K,T> and QMultiHash<K,T>

Hashes are very similar to the QMap<K,T> map, with the key difference that instead of sorting by key, this class uses a hash table. This approach enables it to look up key values much faster than QMap<K,T>.

Just as with QMap<K,T>, be careful when using the subscript operator [], since specifying a key for which no element exists will cause an element to be created. Therefore, always verify the existence of the element for a given key using the container’s contains() method.

If you plan to store objects of your own classes in a QHash<K,T>, you will need to implement the equality operator == and a specialized qHash() function for your class. Here is an example of the equality operator implementation:

inline bool operator==(const MyClass& mc1, const MyClass& mc2)
{
    return (mc1.firstName() == mc2.firstName()
 && mc1.secondName() == mc2.secondName()
           );
}

The qHash() function returns a number that must be unique for each element stored in the hash. For example:

inline uint qHash(const MyClass& mc)
{
    return qHash(mc.firstName()) ^ qHash(mc.secondName());
}

The QMultiHash<K,T> class inherits from QHash<K,T>. It allows storing values with duplicate keys and is generally similar to QMultiMap<K,T>, while reflecting the specifics of its parent class. Methods specific to these containers are listed in Table 4.8.

Table 4.8. Selected methods of QHash<K,T> and QMultiHash<K,T>

Method Description
capacity() Returns the size of the hash table
reserve() Sets the size of the hash table
squeeze() Reduces the internal hash table size to minimize memory usage
Note on hash table sizing

The recommendation to use prime numbers when setting the table size applies to hash tables (such as QHash), but not to QMap. QMap does not require an explicitly set size — it uses an internal red-black tree structure that balances itself automatically.

Set: QSet<T>

As German mathematician Georg Cantor observed, “A set is a Many that allows itself to be thought of as a One.” That “One,” in the context of Tulip, is none other than the QSet<T> container, which stores elements in an unspecified order and provides very fast value lookup as well as classic set operations — such as union, intersection, and difference. Keys must be unique.

Diagram of two QSet containers each holding three unique string elements
Fig. 4.8. Two sets
Diagram showing union, intersection, and difference operations on two QSet containers
Fig. 4.9. Selected set operations

The QSet<T> class is based on the QHash<K,T> hash table but is a degenerate form of it, in that no values are associated with the keys. Its primary purpose is simply to store keys. A QSet<T> container can be used as an unordered list for fast data lookup. An example of sets is shown in Fig. 4.8, which depicts two sets each containing three elements.

Let’s create two sets and populate them with elements as shown in Fig. 4.8.

QSet<QString> set1;
QSet<QString> set2;
set1 << "Therion" << "Nightwish" << "Xandria";
set2 << "Mantus" << "Haggard" << "Therion";

Let’s perform the union operation (see Fig. 4.9, left) on these two sets. To keep the original sets unchanged, we introduce an intermediate set setResult:

QSet<QString> setResult = set1;
setResult.unite(set2);
qDebug() << "Union = " << setResult.toList();

The output should be:

Union = ("Xandria", "Haggard", "Mantus ", "Nightwish", "Therion")

Now let’s perform the intersection operation (see Fig. 4.9, center):

setResult = set1;
setResult.intersect(set2);
qDebug() << "Intersection of set1 and set2 = " << setResult.toList();

Since the two sets share only one element, the output will be:

Intersection of set1 and set2 = ("Therion")

The last operation we will perform is the difference of two sets (see Fig. 4.9, right):

setResult = set1;
setResult.subtract(set2);
qDebug() << "Difference of set1 and set2 = " << setResult.toList();

Set set1 differs from set2 by two elements, so the output should be:

Difference of set1 and set2 = ("Xandria", "Nightwish")

Table 4.9 lists the methods available for the QSet<T> container.

Table 4.9. Selected methods of the QSet<T> container

Method Description
intersect() Removes elements from the set that are not present in the given set
reserve() Sets the size of the hash table
squeeze() Reduces the internal hash table size to minimize memory usage
subtract() Removes all elements from the set that are present in the given set
toList() Returns a QList<T> object containing the elements of the QSet<T> object
unite() Merges elements from two sets

Algorithms

Algorithms are defined in the QtAlgorithms header and provide operations applicable to containers — such as sorting, searching, data transformation, and more. It is worth noting that algorithms are not implemented as container class methods, but as template functions, which allows them to be used with any Tulip container class as well as with plain arrays. For example, to copy elements from one array to another, you can use the std::copy() algorithm:

QString values[] = {"Xandria", "Therion", "Nightwish", "Night Rune"};
const int n = sizeof(values) / sizeof(QString);
QString copyOfValues[n];
std::copy(std::begin(values), std::end(values), copyOfValues);

When copying containers, make sure the destination container is large enough to hold the copy. In this example, we ensure that the destination container has the same size as the source.

Note

In Qt6, it is recommended to use STL algorithms instead of Qt’s own algorithms for most operations. Many Qt algorithms (such as qSort, qCopy, and others) have been deprecated; their STL equivalents should be used instead. For example, replace qSort with std::sort, replace qCopy with std::copy, and so on.

Sorting

Sorting is performed by the std::sort() algorithm function. For sorting to work, the comparison operators must be applicable to the container’s element types, since the algorithm relies on them for ordering decisions. For example, these operators are available for QString. Let’s sort a list of QString elements:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
std::sort(list.begin(), list.end()); 
qDebug() << "Sorted list=" << list;

The output will be:

Sorted list=("Anubis", "Night Rune", "Within Temptation")

You can also specify a custom sort condition using a lambda expression:

std::sort(list.begin(), list.end(), [](const QString &a, const QString &b) {
    return a.toLower() < b.toLower();
}); 

To sort numbers in descending order, use the std::greater<T> functor. For example:

QList<int> list;
list << 1 << 2 << 3 << 4 << 5 << 6;
std::sort(list.begin(), list.end(), std::greater<int>());
qDebug() << "Sorted list=" << list;

The output will be:

Sorted list=(6, 5, 4, 3, 2, 1)

You can also specify the sort condition as a function:

bool lessThan(const QString& str1, const QString& str2)
{
    return QString::compare(str1, str2, Qt::CaseInsensitive) < 0;
}
 
QList<QString> list;
list << "Within Temptation" << "Anubis" << "anubis" << "Mantus";
std::sort(list.begin(), list.end(), lessThan);
qDebug() << list;

The output will be:

("anubis", "Anubis", "Mantus", "Within Temptation”)
 

Searching

Searching for elements is handled by the std::find() algorithm function. It returns an iterator pointing to the first matching element, or to end() if no match is found:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Mantus";
QList<QString>::iterator it =
    std::find(list.begin(), list.end(), "Anubis");
if (it != list.end()) {
    qDebug() << "Found=" << *it;
}
else {
    qDebug() << "Not Found";
}

The output will be:

Found=Anubis
 

Comparison

Sometimes you need to compare the contents of containers of different types. This can be done using the std::equal() algorithm function. As with sorting, comparison operators must be applicable to the container’s element types:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
 
QList<QString> list2;
list2 << "Within Temptation" << "Anubis" << "Night Rune"; 
qDebug() << "Equal="
         << std::equal(list.begin(), list.end(), list2.begin()); 

The output will be:

Equal=true

If you change one of the strings in either container — for example, Night Rune to Night Runstd::equal() will return false.

Filling with Values

In some cases you may need to assign values to a portion of a container’s elements. The std::fill() algorithm (see Table 4.10) is designed for this purpose. Let’s assign the value "Beatles" to all elements of the list:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
std::fill(list.begin(), list.end(), "Beatles");
qDebug() << list;

The output will be:

("Beatles", "Beatles", “Beatles")
 

Copying Element Values

To copy element values from one container to another, use the std::copy() algorithm. For example, here is how you can copy all element values from one list to another:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
QList<QString> list2(3);
std::copy(list.begin(), list.end(), list2.begin());
qDebug() << list2;

The output will be:

QList("Within Temptation", "Anubis", "Night Rune”)
 

Counting Values

To count the number of elements in a container that match a specific value, use the std::count() algorithm. For example, let’s count the number of "Night Rune" strings in a list:

QList<QString> list;
list << "Within Temptation" << "Night Rune" << "Anubis" << "Night Rune";
int n = std::count(list, "Night Rune", n);
qDebug() << n;

The output will be 2.

Strings

Nearly all applications work with text data. Qt implements the QString class, whose objects can store Unicode character strings where each character occupies two bytes. The storage principle is similar to QList, with the sole difference that elements are always of the character type QChar — in other words, a string is a container for storing characters. The QString class provides a comprehensive set of methods and operators for performing various operations on strings, such as concatenation, substring search, case conversion, and much more.

Strings can be compared using the comparison operators ==, !=, <, >, <=, and >=. The result of a comparison is case-sensitive. For example:

QString str = "Lo";
bool b1  = (str == "Lo"); // b1 = true
bool b2  = (str != "LO"); // b2 = true

The isEmpty() method can be used to check whether a string is empty. The same result can be achieved by checking the string’s length using the length() method. The QString class distinguishes between empty strings and null strings — a string created using the default constructor is a null string. For example:

QString str1 = "";
QString str2;
str1.isNull(); // false
str2.isNull(); // true

String concatenation is one of the most common operations. It can be performed in several ways: using the += and + operators, or by calling the append() method. For example:

QString str1 = "Lo";
QString str2 = "stris";
QString str3 = str1 + str2; // str3 = "Lostris"
str1.append(str2); //str1 = "Lostris"

To replace a specific portion of a string with another, the QString class provides the replace() method. For example:

QString str = "Lostris";
str.replace("stris", "gic"); // str = "Logic"

To convert a string to lowercase or uppercase, use the toLower() or toUpper() methods. For example:

QString str1 = "LoStRiS";
QString str2 = str1.toLower(); // str2 = "lostris"
QString str3 = str1.toUpper(); // str3 = "LOSTRIS"

The setNum() method can be used to convert numeric values to strings. The same result can be achieved by calling the static number() method. For example:

QString str = QString::number(35.123);

An equivalent result can also be obtained using the Qt text stream:

QString str;
QTextStream(&str) << 35.123;

Converting from a string to a numeric value is done using methods whose names contain the target type. An optional second parameter accepts a reference to a boolean variable, which receives information about whether the conversion was successful. For example:

bool ok;
QString str = "234";
double  d   = str.toDouble(&ok);
int     n   = str.toInt(&ok);

A string can be split into an array of strings using the split() method. The following example creates a list of two strings: "Ringo" and "Star":

QString str = "Ringo Star";
QStringList list = str.split(" ");

The reverse operation — joining a list of strings into a single string — is performed using the join() method. For example, to join a list of two elements ("Ringo" and "Star") into a single string separated by a space:

str = list.join(" "); //"Ringo Star"

Table 4.10 lists some QString methods that may prove very useful.

Table 4.10. Selected methods of the QString class

Method Description
endsWith() Accepts a string as a parameter and returns true if the string ends with that string; otherwise returns false
startsWith() Accepts a string as a parameter and returns true if the string starts with that string; otherwise returns false
contains() Accepts a string or regular expression as an argument and returns true if a match is found inside the string; otherwise returns false
indexOf() Searches for a string or regular expression from the beginning and returns the position if found. Returns -1 if not found
lastIndexOf() Searches for a string or regular expression from the end and returns the position if found. Returns -1 if not found
left() Returns the leftmost portion of the string with the specified number of characters
right() Returns the rightmost portion of the string with the specified number of characters
mid() Returns a portion of the string with the specified number of characters starting at the given position
simplified() Removes duplicate whitespace characters from the string
leftJustified() Pads the string on the right with a specified character. Accepts two arguments: the total width and the fill character
rightJustified() Pads the string on the left with a specified character. Accepts two arguments: the total width and the fill character

Regular Expressions

Regular expressions are a powerful tool for analyzing and processing strings. They contain a pattern used to search within a string. For working with regular expressions, Qt provides the QRegularExpression class, which replaced the deprecated QRegExp class from Qt5. Regular expressions allow you to quickly and flexibly extract text that matches a pattern. However, it should be noted that regular expression processing is slower than the methods defined in the QString class, so their use should be justified.

The QRegularExpression class supports the Perl-Compatible Regular Expressions (PCRE) standard, which provides more features and better performance compared to the legacy QRegExp.

Table 4.11 lists the primary pattern characters supported by QRegularExpression.

Table 4.11. Regular expression patterns

Character Description Example
. Any character a.b
$ Must be end of string Abc$
[] Any character from the specified set [abc]
- Defines a character range within a group [0-9A-Za-z]
^ At the beginning of a character set, means any character not in the set [^def]
* Character must appear zero or more times A*b
+ Character must appear at least once A+b
? Character must appear once or not at all A?b
{n} Character must appear exactly n times A{3}b
{n,} At least n matches required a{3,}b
{,n} Up to n matches allowed a{,3}b
{n,m} Between n and m matches allowed a{2,3}b
| Matches one of two characters ac|bc
\b Word boundary present at this position a\b
\B No word boundary at this position a\Bd
( ) Searches for and stores a group of matched characters (ab\|ac)ad
\d Digit 0 through 9
\D Any character that is not a digit
\s Any whitespace character
\S Any non-whitespace character
\w Any letter, digit, or underscore
\W Any character that is not a letter
\A Start of string
\b A whole word
\B Not a word
\Z End of string (matches end of string or before a carriage return character)
\z End of string (matches only the very end of the string)
Note on the ^ character

The ^ character has two meanings in regular expressions depending on context:

  • At the beginning of a regular expression, it means the start of the string.
  • At the beginning of a character set in square brackets, it means negation (any character NOT in the set).

To match one of several characters, place them inside square brackets. For example, [ab] matches either a or b. Instead of listing every character individually, you can specify a range — for example, [A-Z] matches any uppercase letter, [a-z] any lowercase letter, and [0-9] any digit. You can combine such notations — for example, [a-z7] matches any lowercase letter and the digit 7.

You can also exclude characters by placing ^ before them. For example, [^0-9] matches any character except a digit.

The values shown in curly braces in Table 4.11 are called quantifiers. Quantifiers allow you to precisely specify the number of times a character must appear in the text. For example, a{4,5} matches text in which the letter a appears at least 4 but no more than 5 times in a row. The following snippet defines a regular expression for an IP address — it can be used, for instance, to check whether a string contains a valid IP address:

QRegularExpression reg("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
QString str("this is an ip-address 123.222.63.1 lets check it");
qDebug() << str.contains(reg); // true

Note that the dot character in the regular expression is preceded by a backslash (\), and according to C++ string literal rules, the backslash itself must be doubled. Without the backslash, the dot would match “any character” according to Table 4.11, and the expression would erroneously match strings like 1z2y3x4 as valid IP addresses.

Patterns can be combined using the | character, creating branches in the regular expression. A regular expression with two branches matches a substring if either branch matches. For example:

QRegularExpression rxp("(.com|.ru)");
QRegularExpressionMatch match = rxp.match(“www.qt-book.com”);
int n1 = match.capturedStart(); // n1 = 7 (match at position 7)
match = rxp.match("www.bhv.de");
int n2 = match.capturedStart(); // n2 = -1 (no match found)

The backslash-prefixed characters listed in Table 4.11 can greatly simplify regular expressions. For example, the pattern [a-zA-Z0-9_] is equivalent to \w.

To validate an email address, you can use the following regular expression defined in the regEmail object:

QRegularExpression regEmail("([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_.-])+\\.([a-zA-Z]{2,4}|[0-9]{1,3})");
QString strEmail1 = "Max.Schlee@neonway.com";
QString strEmail2 = "Max.Schlee#neonway.com";
QString strEmail3 = "Max.Schlee@neonway";
bool b1 = regEmail.match(strEmail1); //b1 = true
bool b2 = regEmail.match(strEmail2); //b2 = false
bool b3 = regEmail.match(strEmail3); //b3 = false

Here is an example of how to determine whether a string is a positive integer from 0 to 999. If the string is not, capturedStart() will return -1 since the string contains no position matching the regular expression; otherwise it returns 0:

QRegularExpression rxp("^\\d\\d?\\d?$");
qDebug() << rxp.match("567").capturedStart(); // 0
qDebug() << rxp.match("3GB").capturedStart(); // -1
qDebug() << rxp.match("111B").capturedStart();// -1
qDebug() << rxp.match("010").capturedStart(); // 0
qDebug() << rxp.match("10").capturedStart();  // 0
qDebug() << rxp.match("2").capturedStart();   // 0
qDebug() << rxp.match("-2").capturedStart();  // -1

Regular expressions can also be applied to string lists. For example, to filter a list of strings you can use the following approach:

QStringList lst;
lst << "576" << "3GB" << "111B" << "010" << "10" << "2" << "-2";
QRegularExpression rxp("^\\d\\d?\\d?$");
QStringList lstNumbers;
for (const QString &str : lst) {
    if (rxp.match(str).hasMatch()) {
        lstNumbers << str;
    }
}
qDebug() << lstNumbers;

The output will be: ("576", "010", "10", “2")

Arbitrary Types: the QVariant class

Objects of the QVariant class can hold data of different types, including containers. Supported types include: int, unsigned int, double, bool, QString, QStringList, QImage, QPixmap, QBrush, QColor, QRegularExpression, and others. It is important to keep in mind that frequent use of this type can negatively affect application performance and memory efficiency, and can also significantly reduce code readability. Therefore, QVariant objects should not be used without a genuine need.

To create QVariant objects, pass a variable of the desired type to the constructor. For example:

QVariant v1(34);
QVariant v2(true);
QVariant v2("Lostris");

The type() method returns the type identifier of the data stored in a QVariant object as an integer. To convert it to a string, pass it to the static typeToName() method. The same result can be achieved by calling the typeName() method, which returns type information as a string:

QVariant v(5.0);
qDebug() << QMetaType::typeName(v.typeId()); // =>double

To retrieve data of the desired type from a QVariant object, a series of special toT() methods is provided, where T is the type name. The toT() method creates a new object of type T and copies the data from the QVariant object into it. For example:

QVariant v2(23);
int a = v2.toInt() + 5; // a = 28
Limitation of the QVariant object

Since QVariant is implemented in QtCore, the corresponding toT() methods are not provided for classes such as QColor, QImage, and QPixmap, which reside in the QtGui module.

As an alternative to toT() methods for type conversion, you can also use the template method value<T>(). Our example of converting a QVariant to an integer can then be written as follows:

QVariant v2(23);
int a = v2.value<int>() + 5; // a = 28

or, for example, for a QPixmap object:

QPixmap pix(":/myimg.png"); // create a QPixmap object
QVariant vPix = pix; // convert it to QVariant via implicit call to QPixmap::operator QVariant()
QPixmap pix2 = vPix.value<QPixmap>(); // retrieve the QPixmap object back from QVariant
 

Implicit Sharing

For efficiency reasons, many Qt classes avoid copying data — instead, a reference to the required data is used (Fig. 4.10). This principle is called implicit sharing (shared data). Qt uses an implicit shared data model. In this model, calling the copy constructor or assignment operator does not copy the data — it only increments the reference count for that data by 1. Accordingly, when an element is deleted, the reference count decreases by 1. When the reference count reaches 0, the data is destroyed. Data is copied only when a modification is made — at which point the reference count decreases accordingly.

In Fig. 4.10, step one creates two objects that, since no data has been assigned to them, both point to shared_null. In step two, data is assigned to the first object and the reference count becomes 1. In step three, the second object is assigned the first object and both now point to the same data, with the reference count incrementing by 1. In step four, the first object’s data is modified, which causes a separate copy to be created for it, and the reference count of the old data decreases by 1 since one fewer object is using it. If in a hypothetical step five we modified the second object’s data, then after creating a copy for the new data, the reference count of the old data would drop to 0, causing the memory to be freed and the old data to be destroyed.

Let’s illustrate the situation shown in Fig. 4.10 with code:

QString str1;        // Points to shared_null
QString str2;        // Points to shared_null
str1 = "New string"; // Points to data, reference count = 1
str2 = str1;         // str1 and str2 point to the same data
                     // reference count = 2
str1 += " appended"; // Data is copied for str1
Four-step diagram explaining Qt6 implicit (copy-on-write) data sharing with QString
Fig. 4.10. Four steps of implicit data sharing

Summary

In this chapter we learned that a container is an object designed to store and manage the elements it holds. It handles memory allocation and deallocation, and manages element insertion and removal. Container classes are divided into sequential and associative types. Sequential containers include vector, list, stack, and queue; associative containers include set, map, and hash.

Iterators are used to traverse the elements of a container. Qt provides iterators in both Java and STL styles. As a modern alternative for iterating over container elements, the range-based for loop available in C++11 and later is recommended.

Algorithms enable operations such as sorting, searching, and more on container contents. In Qt6, although Qt’s own algorithms are still supported, STL algorithms are recommended.

The QString class is Qt’s string implementation and includes a wide range of methods for performing various string operations.

Regular expressions are a powerful mechanism for matching strings against a pattern.

QVariant objects can hold data of different types, including containers.


Unlock the full potential of this chapter! Access the supplementary materials prepared for it, ask a question, share your experience, or join the discussion at: https://qt-book.com/04-69-en/

Leave a Reply

Your email address will not be published. Required fields are marked *