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:
- container classes (containers);
- algorithms;
- iterators.
Their interrelationship is illustrated in Fig. 4.1.

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:
QList<T>— list;QStack<T>— stack;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:
QSet<T>— set;QMap<K,T>— map;QMultiMap<K,T>— multi-map;QHash<K,T>— hash;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:
- Java-style iterators;
- 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 |
| Use the empty() method to check whether a container holds any elements
If you need to check whether a container is empty, use |
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).

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

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).

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>.

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).


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 |
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.


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 |
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 Run — std::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
|
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 |
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

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/
