Search the Catalog
Programming with Qt, 2nd Edition

Programming with Qt, 2nd Edition

Writing Portable GUI applications on Unix and Win32

By Matthias Kalle Dalheimer
2nd Edition March 2002 
0-596-00064-2, Order Number: 0642
520 pages, $39.95 US $59.95 CA £28.50 UK

Chapter 8
Container Classes

Contents:

Available Container Classes
Choosing a Container Class
Working with Reference-Based Container Classes
Working with Value-Based Container Classes

Container classes were one of the first types of classes that were put into class libraries. Container classes are used for objects that contain other objects, such as arrays or lists. Most GUI class libraries have included container classes.

Note that if you have experience in other programming languages that are commonly used for GUI programming, such as Java, you might understand the notion of "container" differently. In the Java AWT, containers are objects that can hold other GUI objects. In Qt, there is no such difference; every GUI object can hold other GUI objects, even though it does not always make sense. For example, having a button contain other GUI objects would be of no use. Qt uses the classic C++ notion of container classes that can hold other objects in the sense of memory references, not in the more specific sense of general containment.

With the advent of the C++ standard, another library with container classes, the Standard Template Library (STL), has been included in the standard. This standard will lead to all compiler vendors including it.

Nevertheless, it still pays to learn how to use Qt's own container classes. STL is not easy to learn, and when all you want to do is put some strings in a list and iterate over it, you can just as well use the Qt classes. Of course, these classes are not as sophisticated as the STL classes, and there aren't quite as many of them. However, if Qt contains what you need, I would suggest giving it a try. You could even save some memory in your application, since the container classes from Qt are linked to your program anyway, and it is reasonable to assume that compiler vendors will make linkage to the STL optional.

An important thing to distinguish about container classes is whether they are value based or reference based. Value based containers store a copy of whatever you put into them, and when you ask for the item back, you get another copy. This result can lead to a lot of copying, though, and is not always what you want. On the other hand, you never need to worry about ownership (i.e., who is allowed to do what with the item in the container) because all items are copies. Reference-based containers, on the other hand, only store a pointer to the item in question. This storage has the advantage that no copying needs to occur (and is the only option for classes for which copying is explicitly prohibited -- i.e., the copy constructor and the assignment operator are made private). The disadvantage is that you need to be careful about ownership: who has the right to delete a pointer in the container? The container itself or some owner outside? If it is not the container, you must remember that you need to take the pointer out of the container when you delete the corresponding object. Otherwise, you have so-called dangling pointers in your container, indicating that a disaster is bound to happen.

Whether you should choose a reference-based or a value-based container class depends completely on the task at hand, but it is good to know that both exist in Qt (unlike in STL, for example).

Available Container Classes

Qt provides the following container classes. The next section gives you hints about when to choose which class.

QMemArray

This class is very restricted and can be used only for built-in types or classes that have no constructors, destructors, or virtual methods, since it uses bitwise copy to duplicate its members. It is therefore an especially restricted variant of a value-based container. If you need something more flexible, use QIntDict or QPtrList. QMemArray is the only container class that stores its elements directly as a copy; all others store only pointers to their elements. Two derived classes, QByteArray and QPointArray, are used to store bytes and coordinate pairs, respectively. In addition, QString is derived from QMemArray, even though you usually won't use it like that.

QAsciiCache

This class is a cache (see QCache) that is based on char* keys instead of QString keys. The main difference is that QAsciiCache keys can contain only characters in the ASCII character set (plus some European characters), while QCache keys can contain all Unicode characters.

QBitArray

This class is like QMemArray, but contains only bits. It is useful for collecting flags in an application.

QByteArray

Again, this class is like QMemArray, but contains bytes. It could, among other things, be useful to represent some memory of a fixed size, such as in hardware.

QCache

This class provides a hashtable like QPtrDict, but stores only a certain amount of data in it. Entries can get lost if the cache becomes larger than a threshold, and they have to be restored by the application, if necessary.

QPtrDict

QPtrDict provides a so-called hashtable, which is a structure in which the elements are accessed via string keys. This class is very useful when you have pairs of strings and have to look up one of the values by means of the other, for example. QPtrDict is reference based.

QIntCache

This class is similar to QCache, but uses long values instead of QString values for the keys.

QIntDict

This class is similar to QPtrDict, but uses long values instead of QString values for the keys. In a way, this class resembles classical arrays. QIntDict is reference based.

QPtrList

This class provides doubly linked lists, which are a very common data structure in computer science. It is very efficient to use when you want to access the members sequentially in insertion order (or the other way around), but not as efficient when you want to have random access to the members. QPtrList is reference based.

Three classes are derived from QPtrList: QStrList, QStrIList, and QSortedList. The first two store strings, of which the latter uses case-insensitive compare operations.

QSortedList provides automated sorting based on operator< and operator==. Normally, to achieve a sorted list, you need to subclass from QPtrList and reimplement the method compareItems(). However, if the items you put into the list implement operator< and operator== in a way that is useful for sorting, you can just use QSortedList.

QMap

QMap is the value-based equivalent to QPtrDict -- i.e., it provides a hashtable with string keys.

QPtrDict

This class is similar to QPtrDict, but uses void* values instead of QString values for the keys. QPtrDict is reference based.

QPtrQueue

This class implements the well-known data structure "queue," which is used when you want to put in elements at one end and take them out in the same order from the other end. You only have access to the elements whose "turn" it is at the other end of the queue. QPtrQueue is reference based.

QPtrStack

This class implements the well-known data structure "stack," which is used when you want to put in elements at one end and take them out in the opposite order from the same end. You only have access to the end where you put in the elements. QPtrStack is reference based.

QStringList

QStringList is the value-based counterpart to QStrList. Copying strings is cheap in Qt, as they are implicitly shared, which means that the copy is only made when really needed. Therefore, you can use QStringList without the performance penalty that value-based containers usually imply, but get all the advantages. There are actually very few situations when you should choose a QStrList instead of a QStringList.

QValueList

As the name implies, QValueList is the value-based counterpart to QPtrList. It implements a doubly linked list, as does QPtrList.

QValueStack

QValueStack is the value-based counterpart to QPtrStack. See QPtrStack for a description of stack behavior.

QPtrVector

QPtrVector is the reference-based counterpart to QMemArray. Unlike with QMemArray, you can store (pointers to) any object in a vector; as with QMemArray, you need to specify the size yourself and resize the vector, if necessary.

Choosing a Container Class

Deciding which container class to use in a given situation is not always easy. QMemArray and QBitArray can be ruled out whenever you have nontrivial objects to store. The use of the classes mainly depends on how you put the data in, and especially on, how you plan to retrieve them again. If you know that you will retrieve the objects in exactly the same order you put them in, use the reference-based QPtrQueue; there is no value-based counterpart to this class yet. If you know that you will take them out in exactly the opposite order, use the reference-based QPtrStack or its value-based counterpart QValueStack. These classes are optimized for their respective intended uses.

If you will mainly add new members at the end and plan to retrieve the members one after the other (and probably in the same order in which you put them), use the reference-based QPtrList or its value-based counterpart QValueList. On the other hand, if you need maximum flexibility for storage and retrieval of the elements, use the reference-based QPtrDict, QIntDict, or QPtrDict, or their value-based counterpart QMap. Which one you choose should depend on how you want to access the elements. If you access them by an integer number, use QIntDict; if you access them by a string, use QPtrDict; if you access them by another object, use QPtrDict. QMap lets you use any type of key, since you can specify that yourself.

Finally, if there can be a very large number of entries, it is easy for your application to restore entries no longer in the container. If this is the case, the reference-based QAsciiCache, QCache, or QIntCache might be a good choice because they automatically delete the least recently used item from the cache if the size of the cache exceeds a certain threshold (see "Caching Data").

Working with Reference-Based Container Classes

In this section, we will explain how you use the reference-based container classes in your own programs and provide you with some useful code snippets.

Basic Usage

After you have chosen a container class to use, you have to know how to create objects of this class and how to insert and retrieve members.

In this section, we describe only the reference-based classes. The techniques for working with the value-based classes are completely different. There is no technical reason why this is so; the explanation given by the authors of Qt is that since these two types of containers are so different, their interface should be different as well. We are not so convinced by this explanation, but we have to live with it anyway. The next section covers how to use value-based container classes.

Since the reference-based container classes simply store a pointer to the contained object, it would suffice if all classes accepted void*. This would keep type safety to a minimum when using container classes, though, so you have to define a type that accepts pointers to one kind of object. If you know about templates in C++, you probably suspect that Qt uses them as well, which is exactly the case.[1]

[1]Earlier versions of Qt could use macros instead of templates; this is no longer supported.

We will explain the use of container classes with the class QPtrList. Use of the other classes is basically the same; other classes have slightly different methods for insertion, retrieval, and removal.

Let's assume that you are writing a program for managing your stamp collection. You might have already created a class Stamp, and now need a container in which to store objects of this type. Here's how you declare a type StampList with templates:

typedef QPtrList<Stamp> StampList;

This example says that StampList is a special kind of QPtrList that accepts pointers to Stamp objects as its members. It works only if Stamp has already been defined; a forward reference is not enough. Note that you do not have to provide the asterisk to indicate that pointers are to be stored. The reference-based container classes already assume that you want to store a pointer because that is the only thing they can store. Using the asterisk would still be legal C++ code:

typedef QPtrList<Stamp*> StampList;

This line of code means that StampList stores pointers to Stamp objects.

After you create the appropriate type, you can create an object of the container class and insert items in it:

StampList stamplist; 
... 
Stamp stamp1; 
Stamp stamp2; 
Stamp stamp3; 
... 
stamplist.append( &stamp1 ); 
stamplist.insert( 0, &stamp2 ); 
stamplist.insert( 1, &stamp3 );

The append() method inserts items at the end. When using insert(), you have to pass the position at which the item should be inserted.

When you use one of the index containers QPtrDict, QIntDict, and QPtrDict, pass the key as the first parameter and the value as the second parameter to insert().

Members are taken out of a collection with remove() (see the Qt reference documentation for the correct parameters).

For retrieval in dictionary classes, use find() or the synonymous operator[]:

QPtrDict<Stamp> stampdict; 
... 
Stamp stamp1; 
... 
stampdict.insert( "somekey", &stamp1 ); 
Stamp* result = stampdict["somekey"]; 
...

For QPtrList, your best bet is to start at the beginning with first(), or at the end with last(), and then retrieve the elements one by one with next() or prev(). You know you are at the end of the list when one of these methods returns a 0 pointer.

All container classes support the methods clear(), which removes all elements from the container, and count(), which returns the number of stored elements.

"Autodeletion" is a very interesting feature. By default, it is turned off, but when turned on in the constructor call or with a call to setAutoDelete( true ), elements that are removed from the container (either with remove() or when the container itself is deleted) are deleted themselves automatically. For this feature to work, these elements must have been created on the heap with new. This creation allows the use of Java-style insertions:

typedef QPtrList<Stamp> StampList; 
... 
StampList* stamplist = new StampList(); 
stamplist->setAutoDelete( true ); 
... 
list->append( new Stamp( "Blue Mauritius", 3000000 ) ); 
list->append( new Stamp( "Tre Skilling Banco", 1800000 ) ); 
... 
delete stamplist; // the two Stamp objects are deleted automatically

Caching Data

QAsciiCache, QCache, and QIntCache are three useful classes that can help you reduce memory consumption by limiting the size that all the entries may consume together. At first, they seem to work like QPtrDict and QIntDict, except that you pass a so-called cost for each entry. Apart from this, the cache classes work like the dictionary classes until the point where inserting a new item would make the sum cost of all inserted entries larger than a predefined maximum cost set with setMaxCost(). At this point, the cache deletes the items that have not been retrieved from the cache for the longest time to make space for the newly inserted entry. This step has two consequences: first, the cache object must have ownership over the inserted objects so it can delete them when it needs to. You must not delete any cached objects yourself. Second, you must be able to restore cached data from another source because it could be deleted at any time. This other source is usually a disk file or a network connection.

By default, retrieving a cached object with either find() or operator[] makes it the least recently used entry. This assumes a cache-access pattern in which all cached objects are likely to be retrieved in turn, or in which all cached objects have the same probability of being retrieved. If this pattern does not fit your application because retrieving an object from the cache increases the probability that it will be retrieved again in the near future (or at least does not reduce the probability), you can pass false as the second parameter to find(). This pass prevents the cached object from being marked as the least recently used. In this case, operator[] cannot be used.

Iterators

Often, you'll want to traverse all elements in a container. While you could do this using a QPtrList with next() or prev(), it is better to use iterators. Iterators are special classes for traversing all elements in a container. They are safer and can even work with containers that are modified at the same time without getting out of sync. For the dictionary classes, iterators are the only option for complete traversal.

An iterator's type must fit the container it should iterate over exactly. Therefore, there are iterator templates called QDictIterator, QIntDictIterator, QPtrDictIterator, QPtrListIterator, QCacheIterator, and QIntCacheIterator. These templates all have a method toFirst(), which can be used to get the first element in the iteration, and an operator++ for moving one element ahead. QPtrListIterator also has toLast() and operator-- for moving in the opposite direction. You get the current element with current(). This example uses a QPtrListIterator to iterate over all elements in a QPtrList:

typedef QPtrList<Stamp> StampList; 
StampList stamplist; 
stamplist.setAutoDelete( true ); 
stamplist.append( new Stamp( "Blue Mauritius", 3000000 ) ); 
stamplist.append( new Stamp( "Tre Skilling Banco", 1800000 ) ); 
... 
typedef QPtrListIterator<Stamp> StampListIterator; 
StampListIterator myiterator( stamplist ); 
for( myiterator.toFirst(); myiterator.current(); ++myiterator ) 
{ 
  qDebug( "%s is worth %d\n", myiterator.current()->name(), 
      myiterator.current()->value() ); 
}

Note that the type of the iterator fits the type of the container iterated on exactly, and that the container is passed as an argument in the iterator's constructor.

The dictionary containers do not have an intrinsic order, so you will never know which order the members will come back in when iterating over the container. If you need to maintain the order, use QPtrList.

There is a danger associated with the incorrect use of iterators. If you retrieve the current object and do a "dangerous operation," such as removing this object from the container, incrementing the iterator will skip over the next object in question, thus leaving it out. Therefore, you should always increment the iterator right after taking the current element. Here is some boilerplate code that you can use:

QPtrList<X>* mylist; 
QPtrListIterator<X> iterator( *mylist ); 
X *obj; 
 
while( ( obj = iterator.current() ) ) { 
    ++iterator; // iterator now points to the next object to iterate on 
    obj->doAnyOperation(); // even "dangerous" ones 
}

Stacks and Queues

Since QPtrStack and QPtrQueue are used a little bit differently, we treat them to a section of their own. The definition of an appropriate type and the construction of a container object work exactly the same way as in the other classes. Only insertion and retrieval are different. Since QPtrStack allows insertion and retrieval on only one end, just two methods are needed: push() and pop(). push() inserts one element on the stack and pop() retrieves the topmost element from the stack that is no longer on the stack afterwards. The remove() method removes the top-most element without returning it.

QPtrQueue inserts members at one end and takes them out again at the other end. But you don't have to worry about the different sides, just use enqueue() for insertion and dequeue() for deletion. If you simply want to take the element in the front from the queue without knowing what it is, you can also use remove(). The methods count() and clear() are available with stacks and queues as well.

Working with Value-Based Container Classes

We assume here that you have read the previous section about reference-based container classes as well and will just point out the things that are different. The most striking difference is the ubiquitous use of iterators with value-based container classes in Qt. Obviously, this scheme was inspired by the Standard Template Library (STL). While there are iterator classes (as with reference-based containers), an iterator is not so much considered an object of its own (even though it technically is just that), but a pointer to some position in the container.

The simplest case, inserting items one after the other, can still be done without iterators. It looks very similar to the reference-based version:

typedef QValueList<Stamp> ValueStampList;
ValueStampList stamplist; 
... 
Stamp stamp1; 
Stamp stamp2; 
Stamp stamp3; 
... 
stamplist.append( stamp1 ); 
stamplist.append( stamp2 ); 
stamplist.append( stamp3 );

Note how we use append here -- i.e., we append the stamps to the end of the list.

For removing an item, you already need an iterator. Let's take an easy case first and say that we want to remove the first item in the list. We get an iterator pointing to the first item by calling begin() on the list:

stamplist.remove( stamplist.begin() );

You might suspect now that to remove the last item from the list, you need to use end() instead of begin():

// WRONG!
stamplist.remove( stamplist.end() );

since the iterator returned by end() does not point to the last element of the list, but rather behind the last element where no more elements are found. The correct function in this case would be fromLast(), which returns an iterator pointing to the last element.

To insert an element before another one, use insert() and pass it both an iterator pointing to the element before which the new one should be inserted and the new one. To insert a new element at the beginning of the list, you would do as follows:

...
Stamp stamp4;
stamplist.insert( stamplist.begin(), stamp4 );

Remember that we said that the iterator returned by end() points behind the last element in the list? Thus, the following two operations are equivalent:

// Appends to the list
stamplist.append( somestamp );

and:

// Appends to the list as well
stamplist.insert( stamplist.end(), somestamp );

Many methods, such as find(), return an iterator. To get at the object that actually hides behind this iterator, just use the ordinary dereferencing operator *:

Stamp somestamp = *( stamplist.find( someotherstamp ) );

It is probably no surprise that iterating over a value-based container involves iterators as well. The pattern is exemplified here with stamp again:

...
typedef QValueListIterator<Stamp> StampListIterator;
...
for( StampListIterator it = stamplist.begin(); it != stamplist.end(); ++it ) 
  // do something with *it

This example basically says to loop over all iterator values starting with the one pointing to the first element in the container and continue to do so until the last element has been seen (i.e., until the iterator points behind it).

Remember that even here, you need to dereference the iterator to get at the value to which it points.

Back to: Programming with Qt, 2nd Edition


oreilly.com Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies | Privacy Policy

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com