Chapter 11. Container Classes

Container Classes

Container classes are general-purpose template classes that store items of a given type in memory. C++ already offers many containers as part of the Standard Template Library (STL), which is included in the Standard C++ library.

Qt provides its own container classes, so for Qt programs we can use both the Qt and the STL containers. The main advantages of the Qt containers are that they behave the same on all platforms and that they are implicitly shared. Implicit sharing, or “copy on write”, is an optimization that makes it possible to pass entire containers as values without any significant performance cost. The Qt containers also feature easy-to-use iterator classes inspired by Java, they can be streamed using QDataStream, and they usually result in less code in the executable than the corresponding STL containers. Finally, on some hardware platforms supported by Qt/Embedded Linux, the Qt containers are the only ones available.

Qt offers both sequential containers such as QVector<T>, QLinkedList<T>, and QList<T>, and associative containers such as QMap<K, T> and QHash<K, T>. Conceptually, the sequential containers store items one after another, whereas the associative containers store key–value pairs.

Qt also provides generic algorithms that perform operations on arbitrary containers. For example, the qSort() algorithm sorts a sequential container, and qBinaryFind() performs a binary search on a sorted sequential container. These algorithms are similar to those offered by the STL.

If you are already familiar with the STL containers and have STL available on your target platforms, you might want to use them instead of, or in addition to, the Qt containers. For more information about the STL classes and functions, a good place to start is SGI’s STL web site: http://www.sgi.com/tech/stl/.

In this chapter, we will also look at QString, QByteArray, and QVariant, since they have a lot in common with containers. QString is a 16-bit Unicode string used throughout Qt’s API. QByteArray is an array of 8-bit chars useful for storing raw binary data. QVariant is a type that can store most C++ and Qt value types.

Sequential Containers

A QVector<T> is an array-like data structure that stores its items at adjacent positions in memory, as Figure 11.1 illustrates. What distinguishes a vector from a plain C++ array is that a vector knows its own size and can be resized. Appending extra items to the end of a vector is fairly efficient, whereas inserting items at the front or in the middle of a vector can be expensive.

A vector of doubles

Figure 11.1. A vector of doubles

If we know in advance how many items we are going to need, we can give the vector an initial size when we define it and use the [] operator to assign a value to the items; otherwise, we must either resize the vector later on or append items. Here’s an example where we specify the initial size:

QVector<double> vect(3);
vect[0] = 1.0;
vect[1] = 0.540302;
vect[2] = -0.416147;

Here’s the same example, this time starting with an empty vector and using the append() function to append items at the end:

QVector<double> vect;
vect.append(1.0);
vect.append(0.540302);
vect.append(-0.416147);

We can also use the << operator instead of append():

vect << 1.0 << 0.540302 << -0.416147;

One way to iterate over the vector’s items is to use [] and count():

double sum = 0.0;
for (int i = 0; i < vect.count(); ++i)
    sum += vect[i];

Vector entries that are created without being assigned an explicit value are initialized using the item class’s default constructor. Basic types and pointer types are initialized to zero.

Inserting items at the beginning or in the middle of a QVector<T>, or removing items from these positions, can be inefficient for large vectors. For this reason, Qt also offers QLinkedList<T>, a data structure that stores its items at non-adjacent locations in memory, as illustrated by Figure 11.2. Unlike vectors, linked lists don’t support random access, but they provide “constant time” insertions and removals.

A linked list of doubles

Figure 11.2. A linked list of doubles

Linked lists do not provide the [] operator, so iterators must be used to traverse their items. Iterators are also used to specify the position of items. For example, the following code inserts the string “Tote Hosen” between “Clash” and “Ramones”:

QLinkedList<QString> list;
list.append("Clash");
list.append("Ramones");

QLinkedList<QString>::iterator i = list.find("Ramones");
list.insert(i, "Tote Hosen");

We will take a more detailed look at iterators later in this section.

The QList<T> sequential container is an “array-list” that combines the most important benefits of QVector<T> and QLinkedList<T> in a single class. It supports random access, and its interface is index-based like QVector’s. Inserting or removing an item at either end of a QList<T> is very fast, and inserting in the middle is fast for lists with up to about one thousand items. Unless we want to perform insertions in the middle of huge lists or need the list’s items to occupy consecutive addresses in memory, QList<T> is usually the most appropriate general-purpose container class to use.

The QStringList class is a subclass of QList<QString> that is widely used in Qt’s API. In addition to the functions it inherits from its base class, it provides some extra functions that make the class more versatile for string handling. We discuss QStringList in the last section of this chapter (p. 290).

QStack<T> and QQueue<T> are two more examples of convenience subclasses. QStack<T> is a vector that provides push(), pop(), and top(). QQueue<T> is a list that provides enqueue(), dequeue(), and head().

For all the container classes seen so far, the value type T can be a basic type like int or double, a pointer type, or a class that has a default constructor (a constructor that takes no arguments), a copy constructor, and an assignment operator. Classes that qualify include QByteArray, QDateTime, QRegExp, QString, and QVariant. Qt classes that are derived from QObject do not qualify, because they lack a copy constructor and an assignment operator. This is no problem in practice, since we can simply store pointers to QObject types rather than the objects themselves.

The value type T can also be a container, in which case we must remember to separate consecutive angle brackets with spaces; otherwise, the compiler will choke on what it thinks is a >> operator. For example:

QList<QVector<double> > list;

In addition to the types just mentioned, a container’s value type can be any custom class that meets the criteria described earlier. Here is an example of such a class:

class Movie
{
public:
    Movie(const QString &title = "", int duration = 0);

    void setTitle(const QString &title) { myTitle = title; }
    QString title() const { return myTitle; }
    void setDuration(int duration) { myDuration = duration; }
    QString duration() const { return myDuration; }

private:
    QString myTitle;
    int myDuration;
};

The class has a constructor that requires no arguments (although it can take up to two). It also has a copy constructor and an assignment operator, both implicitly provided by C++. For this class, a member-by-member copy is sufficient, so there is no need to implement our own copy constructor and assignment operator.

Qt provides two categories of iterators for traversing the items stored in a container: Java-style iterators and STL-style iterators. The Java-style iterators are easier to use, whereas the STL-style iterators can be combined with Qt’s and STL’s generic algorithms and are more powerful.

For each container class, there are two Java-style iterator types: a read-only iterator and a read-write iterator. Their valid positions are shown in Figure 11.3. The read-only iterator classes are QVectorIterator<T>, QLinkedListIterator<T>, and QListIterator<T>. The corresponding read-write iterators have Mutable in their name (e.g., QMutableVectorIterator<T>). In this discussion, we will concentrate on QList’s iterators; the iterators for linked lists and vectors have the same API.

Valid positions for Java-style iterators

Figure 11.3. Valid positions for Java-style iterators

The first thing to keep in mind when using Java-style iterators is that they don’t point directly at items. Instead, they can be located before the first item, after the last item, or between two items. A typical iteration loop looks like this:

QList<double> list;
...
QListIterator<double> i(list);
while (i.hasNext()) {
    do_something(i.next());
}

The iterator is initialized with the container to traverse. At this point, the iterator is located just before the first item. The call to hasNext() returns true if there is an item to the right of the iterator. The next() function returns the item to the right of the iterator and advances the iterator to the next valid position.

Iterating backward is similar, except that we must first call toBack() to position the iterator after the last item:

QListIterator<double> i(list);
i.toBack();
while (i.hasPrevious()) {
    do_something(i.previous());
}

The hasPrevious() function returns true if there is an item to the left of the iterator; previous() returns the item to the left of the iterator and moves the iterator back by one position. Another way to think about the next() and previous() iterators is that they return the item that the iterator has just jumped over, as Figure 11.4 illustrates.

Effect of previous() and next() on a Java-style iterator

Figure 11.4. Effect of previous() and next() on a Java-style iterator

Mutable iterators provide functions to insert, modify, and remove items while iterating. The following loop removes all the negative numbers from a list:

QMutableListIterator<double> i(list);
while (i.hasNext()) {
    if (i.next() < 0.0)
        i.remove();
}

The remove() function always operates on the last item that was jumped over. It also works when iterating backward:

QMutableListIterator<double> i(list);
i.toBack();
while (i.hasPrevious()) {
    if (i.previous() < 0.0)
        i.remove();
}

Similarly, the mutable Java-style iterators provide a setValue() function that modifies the last item that was jumped over. Here’s how we would replace negative numbers with their absolute value:

QMutableListIterator<double> i(list);
while (i.hasNext()) {
    int val = i.next();
    if (val < 0.0)
        i.setValue(-val);
}

It is also possible to insert an item at the current iterator position by calling insert(). The iterator is then advanced to point between the new item and the following item.

In addition to the Java-style iterators, every sequential container class C<T> has two STL-style iterator types: C<T>::iterator and C<T>::const_iterator. The difference between the two is that const_iterator doesn’t let us modify the data.

A container’s begin() function returns an STL-style iterator that refers to the first item in the container (e.g., list[0]), whereas end() returns an iterator to the “one past the last” item (e.g., list[5] for a list of size 5). Figure 11.5 shows the valid positions for STL-style iterators. If a container is empty, begin() equals end(). This can be used to see whether the container has any items, although it is usually more convenient to call isEmpty() for this purpose.

Valid positions for STL-style iterators

Figure 11.5. Valid positions for STL-style iterators

The STL-style iterator syntax is modeled after that of C++ pointers into an array. We can use the ++ and -- operators to move to the next or previous item, and the unary * operator to retrieve the current item. For QVector<T>, the iterator and const_iterator types are merely typedefs for T * and const T *. (This is possible because QVector<T> stores its items in consecutive memory locations.)

The following example replaces each value in a QList<double> with its absolute value:

QList<double>::iterator i = list.begin();
while (i != list.end()) {
    *i = qAbs(*i);
    ++i;
}

A few Qt functions return a container. If we want to iterate over the return value of a function using an STL-style iterator, we must take a copy of the container and iterate over the copy. For example, the following code is the correct way to iterate over the QList<int> returned by QSplitter::sizes():

QList<int> list = splitter->sizes();
QList<int>::const_iterator i = list.begin();
while (i != list.end()) {
    do_something(*i);
    ++i;
}

The following code is wrong:

// WRONG
QList<int>::const_iterator i = splitter->sizes().begin();
while (i != splitter->sizes().end()) {
    do_something(*i);
    ++i;
}

This is because QSplitter::sizes() returns a new QList<int> by value every time it is called. If we don’t store the return value, C++ automatically destroys it before we have even started iterating, leaving us with a dangling iterator. To make matters worse, each time the loop is run, QSplitter::sizes() must generate a new copy of the list because of the splitter->sizes().end() call. In summary: When using STL-style iterators, always iterate on a copy of a container returned by value.

With read-only Java-style iterators, we don’t need to take a copy. The iterator takes a copy for us behind the scenes, ensuring that we always iterate over the data that the function first returned. For example:

QListIterator<int> i(splitter->sizes());
while (i.hasNext()) {
    do_something(i.next());
}

Copying a container like this sounds expensive, but it isn’t, thanks to an optimization called implicit sharing. This means that copying a Qt container is about as fast as copying a single pointer. Only if one of the copies is changed is data actually copied—and this is all handled automatically behind the scenes. For this reason, implicit sharing is sometimes called “copy on write”.

The beauty of implicit sharing is that it is an optimization that we don’t need to think about; it simply works, without requiring any programmer intervention. At the same time, implicit sharing encourages a clean programming style where objects are returned by value. Consider the following function:

QVector<double> sineTable()
{
    QVector<double> vect(360);
    for (int i = 0; i < 360; ++i)
        vect[i] = std::sin(i / (2 * M_PI));
    return vect;
}

The call to the function looks like this:

QVector<double> table = sineTable();

STL, in comparison, encourages us to pass the vector as a non-const reference to avoid the copy that takes place when the function’s return value is stored in a variable:

void sineTable(std::vector<double> &vect)
{
    vect.resize(360);
    for (int i = 0; i < 360; ++i)
        vect[i] = std::sin(i / (2 * M_PI));
}

The call then becomes more tedious to write and less clear to read:

std::vector<double> table;
sineTable(table);

Qt uses implicit sharing for all of its containers and for many other classes, including QByteArray, QBrush, QFont, QImage, QPixmap, and QString. This makes these classes very efficient to pass by value, both as function parameters and as return values.

Implicit sharing is a guarantee from Qt that the data won’t be copied if we don’t modify it. To get the best out of implicit sharing, we can adopt a couple of new programming habits. One habit is to use the at() function rather than the [] operator for read-only access on a (non-const) vector or list. Since Qt’s containers cannot tell whether [] appears on the left side of an assignment or not, it assumes the worst and forces a deep copy to occur—whereas at() isn’t allowed on the left side of an assignment.

A similar issue arises when we iterate over a container with STL-style iterators. Whenever we call begin() or end() on a non-const container, Qt forces a deep copy to occur if the data is shared. To prevent this inefficiency, the solution is to use const_iterator, constBegin(), and constEnd() whenever possible.

Qt provides one last method for iterating over items in a sequential container: the foreach loop. It looks like this:

QLinkedList<Movie> list;
...
foreach (Movie movie, list) {
    if (movie.title() == "Citizen Kane") {
        std::cout << "Found Citizen Kane" << std::endl;
        break;
    }
}

The foreach pseudo-keyword is implemented in terms of the standard for loop. At each iteration of the loop, the iteration variable (movie) is set to a new item, starting at the first item in the container and progressing forward. The foreach loop automatically takes a copy of the container when the loop is entered, and for this reason the loop is not affected if the container is modified during iteration.

The break and continue loop statements are supported. If the body consists of a single statement, the braces are unnecessary. Just like a for statement, the iteration variable can be defined outside the loop, like this:

QLinkedList<Movie> list;
Movie movie;
...
foreach (movie, list) {
    if (movie.title() == "Citizen Kane") {
        std::cout << "Found Citizen Kane" << std::endl;
        break;
    }
}

Defining the iteration variable outside the loop is the only option for containers that hold data types that contain a comma (e.g., QPair<QString, int>).

Associative Containers

An associative container holds an arbitrary number of items of the same type, indexed by a key. Qt provides two main associative container classes: QMap<K, T> and QHash<K, T>.

A QMap<K, T> is a data structure that stores key–value pairs in ascending key order, as illustrated in Figure 11.6. This arrangement makes it possible to provide good lookup and insertion performance, and key-order iteration. Internally, QMap<K, T> is implemented as a skip-list.

A map of QString to int

Figure 11.6. A map of QString to int

One simple way to insert items into a map is to call insert():

QMap<QString, int> map;
map.insert("eins", 1);
map.insert("sieben", 7);
map.insert("dreiundzwanzig", 23);

Alternatively, we can simply assign a value to a given key as follows:

map["eins"] = 1;
map["sieben"] = 7;
map["dreiundzwanzig"] = 23;

The [] operator can be used for both insertion and retrieval. If [] is used to retrieve a value for a non-existent key in a non-const map, a new item will be created with the given key and an empty value. To avoid accidentally creating empty values, we can use the value() function to retrieve items instead of []:

int val = map.value("dreiundzwanzig");

If the key doesn’t exist, a default value is returned using the value type’s default constructor, and no new item is created. For basic and pointer types, zero is returned. We can specify another default value as the second argument to value(), for example:

int seconds = map.value("delay", 30);

This is equivalent to

int seconds = 30;
if (map.contains("delay"))
    seconds = map.value("delay");

The K and T data types of a QMap<K, T> can be basic data types like int and double, pointer types, or classes that have a default constructor, a copy constructor, and an assignment operator. In addition, the K type must provide an operator<() since QMap<K, T> uses this operator to store the items in ascending key order.

QMap<K, T> has a couple of convenience functions, keys() and values(), that are especially useful when dealing with small data sets. They return QLists of a map’s keys and values.

Maps are normally single-valued: If a new value is assigned to an existing key, the old value is replaced by the new value, ensuring that no two items share the same key. It is possible to have multiple key–value pairs with the same key by using the insertMulti() function or the QMultiMap<K, T> convenience subclass. QMap<K, T> has a values(const K &) overload that returns a QList of all the values for a given key. For example:

QMultiMap<int, QString> multiMap;
multiMap.insert(1, "one");
multiMap.insert(1, "eins");
multiMap.insert(1, "uno");

QList<QString> vals = multiMap.values(1);

A QHash<K, T> is a data structure that stores key–value pairs in a hash table. Its interface is almost identical to that of QMap<K, T>, but it has different requirements for the K template type and usually provides much faster lookups than QMap<K, T> can achieve. Another difference is that QHash<K, T> is unordered.

In addition to the standard requirements on any value type stored in a container, the K type of a QHash<K, T> needs to provide an operator==() and be supported by a global qHash() function that returns a hash value for a key. Qt already provides qHash() functions for integer types, pointer types, QChar, QString, and QByteArray.

QHash<K, T> automatically allocates a prime number of buckets for its internal hash table and resizes this as items are inserted or removed. It is also possible to fine-tune performance by calling reserve() to specify the number of items expected to be stored in the hash and squeeze() to shrink the hash table based on the current number of items. A common idiom is to call reserve() with the maximum number of items we expect, then insert the data, and finally call squeeze() to minimize memory usage if there were fewer items than expected.

Hashes are normally single-valued, but multiple values can be assigned to the same key using the insertMulti() function or the QMultiHash<K, T> convenience subclass.

Besides QHash<K, T>, Qt also provides a QCache<K, T> class that can be used to cache objects associated with a key, and a QSet<K> container that only stores keys. Internally, both rely on QHash<K, T> and both have the same requirements for the K type as QHash<K, T>.

The easiest way to iterate through all the key–value pairs stored in an associative container is to use a Java-style iterator. Because the iterators must give access to both a key and a value, the Java-style iterators for associative containers work slightly differently from their sequential counterparts. The main difference is that the next() and previous() functions return an object that represents a key–value pair, rather than simply a value. The key and value components are accessible from this object as key() and value(). For example:

QMap<QString, int> map;
...
int sum = 0;
QMapIterator<QString, int> i(map);
while (i.hasNext())
    sum += i.next().value();

If we need to access both the key and the value, we can simply ignore the return value of next() or previous() and use the iterator’s key() and value() functions, which operate on the last item that was jumped over:

QMapIterator<QString, int> i(map);
while (i.hasNext()) {
    i.next();
    if (i.value() > largestValue) {
        largestKey = i.key();
        largestValue = i.value();
    }
}

Mutable iterators have a setValue() function that modifies the value associated with the current item:

QMutableMapIterator<QString, int> i(map);
while (i.hasNext()) {
    i.next();
    if (i.value() < 0.0)
        i.setValue(-i.value());
}

STL-style iterators also provide key() and value() functions. With the non-const iterator types, value() returns a non-const reference, allowing us to change the value as we iterate. Note that although these iterators are called “STL-style”, they deviate significantly from the std::map<K, T> iterators, which are based on std::pair<K, T>.

The foreach loop also works on associative containers, but only on the value component of the key–value pairs. If we need both the key and the value components of the items, we can call the keys() and values(const K &) functions in nested foreach loops as follows:

QMultiMap<QString, int> map;
...
foreach (QString key, map.keys()) {
    foreach (int value, map.values(key)) {
        do_something(key, value);
    }
}

Generic Algorithms

The <QtAlgorithms> header declares a set of global template functions that implement basic algorithms on containers. Most of these functions operate on STL-style iterators.

The STL <algorithm> header provides a more complete set of generic algorithms. These algorithms can be used on Qt containers as well as STL containers. If STL implementations are available on all your platforms, there is probably no reason to avoid using the STL algorithms when Qt lacks an equivalent algorithm. Here, we will introduce the most important Qt algorithms.

The qFind() algorithm searches for a particular value in a container. It takes a “begin” and an “end” iterator and returns an iterator pointing to the first item that matches, or “end” if there is no match. In the following example, i is set to list.begin() + 1, whereas j is set to list.end():

QStringList list;
list << "Emma" << "Karl" << "James" << "Mariette";

QStringList::iterator i = qFind(list.begin(), list.end(), "Karl");
QStringList::iterator j = qFind(list.begin(), list.end(), "Petra");

The qBinaryFind() algorithm performs a search just like qFind(), except that it assumes that the items are sorted in ascending order and uses fast binary searching rather than qFind()’s linear searching.

The qFill() algorithm populates a container with a particular value:

QLinkedList<int> list(10);
qFill(list.begin(), list.end(), 1009);

Like the other iterator-based algorithms, we can also use qFill() on a portion of the container by varying the arguments. The following code snippet initializes the first five items of a vector to 1009 and the last five items to 2013:

QVector<int> vect(10);
qFill(vect.begin(), vect.begin() + 5, 1009);
qFill(vect.end() - 5, vect.end(), 2013);

The qCopy() algorithm copies values from one container to another:

QVector<int> vect(list.count());
qCopy(list.begin(), list.end(), vect.begin());

qCopy() can also be used to copy values within the same container, as long as the source range and the target range don’t overlap. In the next code snippet, we use it to overwrite the last two items of a list with the first two items:

qCopy(list.begin(), list.begin() + 2, list.end() - 2);

The qSort() algorithm sorts the container’s items into ascending order:

qSort(list.begin(), list.end());

By default, qSort() uses the < operator to compare the items. To sort items in descending order, pass qGreater<T>() as the third argument (where T is the container’s value type), as follows:

qSort(list.begin(), list.end(), qGreater<int>());

We can use the third parameter to define custom sort criteria. For example, here’s a “less than” comparison function that compares QStrings in a case-insensitive way:

bool insensitiveLessThan(const QString &str1, const QString &str2)
{
    return str1.toLower() < str2.toLower();
}

The call to qSort() then becomes

QStringList list;
...
qSort(list.begin(), list.end(), insensitiveLessThan);

The qStableSort() algorithm is similar to qSort(), except it guarantees that items that compare equal appear in the same order after the sort as before. This is useful if the sort criterion takes into account only parts of the value and the results are visible to the user. We used qStableSort() in Chapter 4 to implement sorting in the Spreadsheet application (p. 92).

The qDeleteAll() algorithm calls delete on every pointer stored in a container. It makes sense only on containers whose value type is a pointer type. After the call, the items are still present in the container as dangling pointers, so normally we should also call clear() on the container. For example:

qDeleteAll(list);
list.clear();

The qSwap() algorithm exchanges the value of two variables. For example:

int x1 = line.x1();
int x2 = line.x2();
if (x1 > x2)
    qSwap(x1, x2);

Finally, the <QtGlobal> header, which is included by every other Qt header, provides several useful definitions, including the qAbs() function, that returns the absolute value of its argument, and the qMin() and qMax() functions, that return the minimum or maximum of two values.

Strings, Byte Arrays, and Variants

QString, QByteArray, and QVariant are three classes that have many things in common with containers and that can be used as alternatives to containers in some contexts. Also, like the containers, these classes use implicit sharing as a memory and speed optimization.

We will start with QString. Every GUI program uses strings, not only for the user interface but often also as data structures. C++ natively provides two kinds of strings: traditional C-style ‘’-terminated character arrays and the std::string class. Unlike these, QString holds 16-bit Unicode values. Unicode contains ASCII and Latin-1 as a subset, with their usual numeric values. But since QString is 16-bit, it can represent thousands of other characters for writing most of the world’s languages. See Chapter 18 for more information about Unicode.

When using QString, we don’t need to worry about such arcane details as allocating enough memory or ensuring that the data is ‘’-terminated. Conceptually, QStrings can be thought of as a vector of QChars. A QString can embed ‘’ characters. The length() function returns the size of the entire string, including embedded ‘’ characters.

QString provides a binary + operator to concatenate two strings and a += operator to append one string to another. Because QString automatically preallocates memory at the end of the string data, building up a string by repeatedly appending characters is very fast. Here’s an example that combines + and +=:

QString str = "User: ";
str += userName + "
";

There is also a QString::append() function that does the same thing as the += operator:

str = "User: ";
str.append(userName);
str.append("
");

A completely different way to combine strings is to use QString’s sprintf() function:

str.sprintf("%s %.1f%%", "perfect competition", 100.0);

This function supports the same format specifiers as the C++ library’s sprintf() function. In the preceding example, str is assigned “perfect competition 100.0%”.

Yet another way to build a string from other strings or from numbers is to use arg():

str = QString("%1 %2 (%3s-%4s)")
      .arg("permissive").arg("society").arg(1950).arg(1970);

In this example, “%1” is replaced by “permissive”, “%2” is replaced by “society”, “%3” is replaced by “1950”, and “%4” is replaced by “1970”. The result is “permissive society (1950s-1970s)”. There are arg() overloads to handle various data types. Some overloads have extra parameters for controlling the field width, the numerical base, or the floating-point precision. In general, arg() is a much better solution than sprintf(), because it is type-safe, fully supports Unicode, and allows translators to reorder the “%n” parameters.

QString can convert numbers into strings using the QString::number() static function:

str = QString::number(59.6);

Or using the setNum() function:

str.setNum(59.6);

The reverse conversion, from a string to a number, is achieved using toInt(), toLongLong(), toDouble(), and so on. For example:

bool ok;
double d = str.toDouble(&ok);

These functions accept an optional pointer to a bool variable and set the variable to true or false depending on the success of the conversion. If the conversion fails, these functions return zero.

Once we have a string, we often want to extract parts of it. The mid() function returns the substring starting at a given position (the first argument) and of up to a given length (the second argument). For example, the following code prints “pays” to the console:[*]

QString str = "polluter pays principle";
qDebug() << str.mid(9, 4);

If we omit the second argument, mid() returns the substring starting at the given position and ending at the end of the string. For example, the following code prints “pays principle” to the console:

QString str = "polluter pays principle";
qDebug() << str.mid(9);

There are also left() and right() functions that perform a similar job. Both accept a number of characters, n, and return the first or last n characters of the string. For example, the following code prints “polluter principle” to the console:

QString str = "polluter pays principle";
qDebug() << str.left(8) << " " << str.right(9);

If we want to find out whether a string contains a particular character, substring, or regular expression, we can use one of QString’s indexOf() functions:

QString str = "the middle bit";
int i = str.indexOf("middle");

This will set i to 4. The indexOf() function returns -1 on failure, and accepts an optional start position and case-sensitivity flag.

If we just want to check whether a string starts or ends with something, we can use the startsWith() and endsWith() functions:

if (url.startsWith("http:") && url.endsWith(".png"))
    ...

The preceding code is both simpler and faster than the following code:

if (url.left(5) == "http:" && url.right(4) == ".png")
    ...

String comparison with the == operator is case-sensitive. If we are comparing user-visible strings, localeAwareCompare() is usually the right choice, and if we want to make the comparisons case-insensitive, we can use toUpper() or toLower(). For example:

if (fileName.toLower() == "readme.txt")
    ...

If we want to replace a certain part of a string by another string, we can use replace():

QString str = "a cloudy day";
str.replace(2, 6, "sunny");

The result is “a sunny day”. The code can be rewritten to use remove() and insert():

str.remove(2, 6);
str.insert(2, "sunny");

First, we remove six characters starting at position 2, resulting in the string “a day” (with two spaces), and then we insert “sunny” at position 2.

There are overloaded versions of replace() that replace all occurrences of their first argument with their second argument. For example, here’s how to replace all occurrences of “&” with “&amp;” in a string:

str.replace("&", "&amp;");

One very frequent need is to strip the whitespace (such as spaces, tabs, and newlines) from a string. QString has a function that eliminates whitespace from both ends of a string:

QString str = "   BOB 	 THE 
DOG 
";
qDebug() << str.trimmed();

String str can be depicted as

   

B

O

B

 

 

T

H

E

  

D

O

G

 

The string returned by trimmed() is

B

O

B

 

 

T

H

E

  

D

O

G

When handling user input, we often also want to replace every sequence of one or more internal whitespace characters with single spaces, in addition to stripping whitespace from both ends. This is what the simplified() function does:

QString str = "   BOB 	 THE 
DOG 
";
qDebug() << str.simplified();

The string returned by simplified() is

B

O

B

 

T

H

E

 

D

O

G

A string can be split into a QStringList of substrings using QString::split():

QString str = "polluter pays principle";
QStringList words = str.split(" ");

In the preceding example, we split the string “polluter pays principle” into three substrings: “polluter”, “pays”, and “principle”. The split() function has an optional second argument that specifies whether empty substrings should be kept (the default) or discarded.

The items in a QStringList can be joined to form a single string using join(). The argument to join() is inserted between each pair of joined strings. For example, here’s how to create a single string that is composed of all the strings contained in a QStringList sorted into alphabetical order and separated by newlines:

words.sort();
str = words.join("
");

When dealing with strings, we often need to determine whether a string is empty or not. This is done by calling isEmpty() or by checking whether length() is 0.

The conversion from const char * strings to QString is automatic in most cases, for example:

str += " (1870)";

Here we add a const char * to a QString without formality. To explicitly convert a const char * to a QString, simply use a QString cast, or call fromAscii() or fromLatin1(). (See Chapter 18 for an explanation of handling literal strings in other encodings.)

To convert a QString to a const char *, use toAscii() or toLatin1(). These functions return a QByteArray, which can be converted into a const char * using QByteArray::data() or QByteArray::constData(). For example:

printf("User: %s
", str.toAscii().data());

For convenience, Qt provides the qPrintable() macro that performs the same as the sequence toAscii().constData():

printf("User: %s
", qPrintable(str));

When we call data() or constData() on a QByteArray, the returned string is owned by the QByteArray object. This means that we don’t need to worry about memory leaks; Qt will reclaim the memory for us. On the other hand, we must be careful not to use the pointer for too long. If the QByteArray is not stored in a variable, it will be automatically deleted at the end of the statement.

The QByteArray class has a very similar API to QString. Functions such as left(), right(), mid(), toLower(), toUpper(), trimmed(), and simplified() exist in QByteArray with the same semantics as their QString counterparts. QByteArray is useful for storing raw binary data and 8-bit encoded text strings. In general, we recommend using QString for storing text rather than QByteArray because QString supports Unicode.

For convenience, QByteArray automatically ensures that the “one past the last” byte is always ‘’, making it easy to pass a QByteArray to a function taking a const char *. QByteArray also supports embedded ‘’ characters, allowing us to use it to store arbitrary binary data.

In some situations, we need to store data of different types in the same variable. One approach is to encode the data as a QByteArray or a QString. For example, a string could hold a textual value or a numeric value in string form. These approaches give complete flexibility, but do away with some of C++’s benefits, in particular type safety and efficiency. Qt provides a much cleaner way of handling variables that can hold different types: QVariant.

The QVariant class can hold values of many Qt types, including QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize, and QString, as well as basic C++ numeric types such as double and int. The QVariant class can also hold containers: QMap<QString, QVariant>, QStringList, and QList<QVariant>.

The item view classes, the database module, and QSettings use variants extensively, allowing us to read and write item data, database data, and user preferences for any QVariant-compatible type. We already saw an example of this in Chapter 3, where we passed a QRect, a QStringList, and a couple of bools as variants to QSettings::setValue(), and retrieved them later as variants.

It is possible to create arbitrarily complex data structures using QVariant by nesting values of container types:

QMap<QString, QVariant> pearMap;
pearMap["Standard"] = 1.95;
pearMap["Organic"] = 2.25;

QMap<QString, QVariant> fruitMap;
fruitMap["Orange"] = 2.10;
fruitMap["Pineapple"] = 3.85;
fruitMap["Pear"] = pearMap;

Here we have created a map with string keys (product names) and values that are either floating-point numbers (prices) or maps. The top-level map contains three keys: “Orange”, “Pear”, and “Pineapple”. The value associated with the “Pear” key is a map that contains two keys (“Standard” and “Organic”). When iterating over a map that holds variant values, we need to use type() to check the type that a variant holds so that we can respond appropriately.

Creating data structures like this can be very seductive since we can organize the data in any way we like. But the convenience of QVariant comes at the expense of efficiency and readability. As a rule, it is usually worth defining a proper C++ class to store our data whenever possible.

QVariant is used by Qt’s meta-object system and is therefore part of the QtCore module. Nonetheless, when we link against the QtGui module, QVariant can store GUI-related types such as QColor, QFont, QIcon, QImage, and QPixmap:

QIcon icon("open.png");
QVariant variant = icon;

To retrieve the value of a GUI-related type from a QVariant, we can use the QVariant::value<T>() template member function as follows:

QIcon icon = variant.value<QIcon>();

The value<T>() function also works for converting between non-GUI data types and QVariant, but in practice we normally use the to...() conversion functions (e.g., toString()) for non-GUI types.

QVariant can also be used to store custom data types, assuming they provide a default constructor and a copy constructor. For this to work, we must first register the type using the Q_DECLARE_METATYPE() macro, typically in a header file below the class definition:

Q_DECLARE_METATYPE(BusinessCard)

This enables us to write code such as this:

BusinessCard businessCard;
QVariant variant = QVariant::fromValue(businessCard);
...
if (variant.canConvert<BusinessCard>()) {
    BusinessCard card = variant.value<BusinessCard>();
    ...
}

Because of a compiler limitation, these template member functions are not available for MSVC 6. If you need to use this compiler, use the qVariantFromValue(), qVariantValue<T>(), and qVariantCanConvert<T>() global functions instead.

If the custom data type has << and >> operators for writing to and reading from a QDataStream, we can register them using qRegisterMetaTypeStreamOperators<T>(). This makes it possible to store preferences of custom data types using QSettings, among other things. For example:

qRegisterMetaTypeStreamOperators<BusinessCard>("BusinessCard");

This chapter focused on the Qt containers, as well as on QString, QByteArray, and QVariant. In addition to these classes, Qt also provides a few other containers. One is QPair<T1, T2>, which simply stores two values and is similar to std::pair<T1, T2>. Another is QBitArray, which we will use in the first section of Chapter 21. Finally, there is QVarLengthArray<T, Prealloc>, a low-level alternative to QVector<T>. Because it preallocates memory on the stack and isn’t implicitly shared, its overhead is less than that of QVector<T>, making it more appropriate for tight loops.

Qt’s algorithms, including a few not covered here, such as qCopyBackward() and qEqual(), are described in Qt’s documentation at http://doc.trolltech.com/4.3/qtalgorithms.html. And for more details of Qt’s containers, including information on their time complexity and growth strategies, see http://doc.trolltech.com/4.3/containers.html.



[*] The convenient qDebug() << arg syntax used here requires the inclusion of the <QtDebug> header file, whereas the qDebug("...", arg) syntax is available in any file that includes at least one Qt header.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset