0% found this document useful (0 votes)
27 views

The Standard Template Library (STL)

The document provides an overview of the Standard Template Library (STL) in C++. It describes the main components of the STL, including container classes that hold objects, generic algorithms that perform common operations on containers, and iterators that link containers to algorithms. Container classes like vector and list provide functionality like storing objects, adding/removing elements, and returning iterators. Generic algorithms like sort and reverse operate on containers via iterators to provide common functionality. The STL aims to simplify programming tasks through these reusable components and their ability to work on different data types.

Uploaded by

DarkHawkESP
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
27 views

The Standard Template Library (STL)

The document provides an overview of the Standard Template Library (STL) in C++. It describes the main components of the STL, including container classes that hold objects, generic algorithms that perform common operations on containers, and iterators that link containers to algorithms. Container classes like vector and list provide functionality like storing objects, adding/removing elements, and returning iterators. Generic algorithms like sort and reverse operate on containers via iterators to provide common functionality. The STL aims to simplify programming tasks through these reusable components and their ability to work on different data types.

Uploaded by

DarkHawkESP
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 8

The Standard Template Library (STL)

http://cplus.about.com/library/weekly/aa101003a.htm The Standard Template Library, or STL, is a collection of container classes, generic algorithms and related components that can greatly simplify many programming tasks in C++. The container classes, as you might surmise from the name, are classes used to hold objects of any particular type. Among the methods of the container classes are methods to add and remove elements, return elements and return iterators. Iterators are an abstraction of pointers. They provide a means to traverse containers and access objects within containers. The generic algorithms are a collection of routines that can be used to perform common operations on container classes, such as sorting, merging and finding. The are called generic because they are independent of the container class used and of the datatype of the objects held in those container classes. The iterators are used to link a container with an algorithm. A container class has methods to return iterators that provide access to its contents. These iterators are provided, as arguments, to an algorithm. The algorithm can then traverse, access and manipulate the contents of the container. This lesson introduces the components of the STL and provides some simple examples. The rest of the lessons of the tutorial will examine each of these components in greater detail. The STL provides container classes that can be used to hold objects, similar to the way an array can be used to hold objects. They can be used to store objects of intrinsic types, such as int or double, class objects defined by C++, such as the string, or user defined objects, such as instances of any user defined class. "So what?" you say. If that was the extent of the usefulness of the container class, it's not clear why they exist or why not to use simple arrays for everything. By the end of this tutorial, you will be a believer, but for now here are a few benefits that the container classes offer over arrays. Container classes handle all sizing and memory allocation issues. You can't write past the end of a container class like you can with an array Container classes are optimized for their intended use. Some allow constant time insertions at their end, some allow this anywhere. Some are better to maintain sorted lists, some better for random access retrievals. We'll learn more on this as we proceed. They contain utility methods such as empty(), size() and clear() that can simplify programming. They contain other methods that all insertion, deletion and copying. They have equality, inequality and assignment operators. You can directly compare two containers or assign on to another. They provide iterators. Iterators are an abstraction of pointers. They allow similar syntax and use, but also have some built-in methods and checks that increase their utility.

The container classes are implemented as class templates. Class templates are discussed in the lesson 35 of the C++ Tutorial. It is not necessary to understand class templates to use the STL, but if you want to understand more about the design of this library you'll need this information. This tutorial only briefly discusses the design of the STL. Before we begin to discuss containers further and begin to discuss the other important components of the STL, such as the generic algorithms, it is necessary to understand a little about quantifying computational costs. We need a measure to understand how an algorithm behaves. Suppose, we run an algorithm on a container containing 5 objects during testing and now we intend to run this algorithm on a container containing 50, 500 or 5 million objects. How long will it take? How do we quantify this? Suppose, we retrieve the first element in a container. How long does it take to retrieve the 79th? The same amount of time? Seventy-nine times as much time? The answers to these questions depend on the particular container or particular algorithm, but there are standard ways of expressing and quantifying these relationships. The computational cost of an algorithm or operation is generally expressed using big-Oh notation. I'm sorry to disappoint any of my readers about the subtitle. The "Oh" indicates the order of the operation. It may be constant, or increase in some other way as the size of the container or number of elements being manipulated increase. The "big" in "big-Oh" indications that we are interest in approximate or order of magnitude estimates. If it takes twice as long to retreive element 79 as element 1, this is still considered a constant time. That's close enough. We're concerned that the retrieval time does not increase quadratically (as a square or power of 2) or

cubically (power of 3) for instance. If that were the case the operation would require on order of 79^2 = 6241 or 79^3 = 493039 times the number of operations as the retrieval of element 1. Constant time, O(1). The cost of the operation is independent of the size of the container or number of elements. As noted above, it may not really be constant, but to a resonable approreasonable it's constant. Linear time, O(N). The cost of the algorithm or operation increases linearly as the number of elements increases. For example, the cost to retrieve element 79 is roughly 79 times the cost to get element 1. Quadratic time, O(N2). The cost of the algorithm or operation increases as the square of the number of elements. This is the case for some crude sorting algorithms. Logarithmic time, O(log N). The cost of the algorithm or operation increases as the log of the number of elements. As a review, logbasenumber = exponent. For example, log10100 = 2 and log1000 = 3. For decimal numbers, logs are often written without explicitly showing the base. For instance, log10X is usually written as log X. You should note that for many algorithms that work by successively dividing the number of elements in half, such as many tree based algorithms, the cost actually increases as log2 N. The cost, however, is usually expressed as O(log N) = O(log10N). Why, because logarithms in different bases of the same number are related by a constant. For now, just remember that operations that increase logarithmically increase more slowly than those that increase linearly. O(N log N). The cost increases on the order of N log N. This is true for some algorithms. For instance, the Fast Fourier Transform, FFT, beloved by electrical engineers world wide, has this property.

How do all these orders relate to each other? Let's look at some examples
Number of elements 10 100 1000 10000

O(1)
constant constant constant constant

O(log N)
1 2 3 4

O(N)
10 100 1000 10000

O(N log N)
10 200 3000 40000

O(N2)
100 10000 1000000 100000000

As you can see, as the number of elements increases, the order of an algorithm becomes very significant. The STL provides both sequence and associative containers. The sequence containers are meant to hold a collection of objects of one type in a linear sequence. This is similar to an array. The STL provides three sequence containers. vector - The vector class provides random access retrievals of objects at any position in constant time. This is useful if your code needs to access elements in an unordered way. You need element 1, then element 5, then element 8752 and so on. The cost (cpu clocks or time needed) of getting element 8752 can be considered to be the same constant as the cost to retreive element 1, for instance. Vector is optimized for insertions and deletions at its end. These are also done in constant time. Insertions or deletions at positions other than the end require linear time.
deque - The deque (pronounces deck) class also provides random access retrievals of objects at any position in constant time. Insertions and deletions at both the front and end of a deque are optimized and occur in constant time. Insertions and deletions in the middle require linear time. list - The list class does not support random access retrievals. Retrievals require linear time. This is because lists are implement with an underlying doubly linked-list structure. A linked-list is a series of nodes connected by previous and next pointers. The "previous" pointer gets you to the previous element; the next pointer to the next. Insertions and deletions at any point are efficient and occur in constant time. Insertion or deletion requires onlreassignmentnt of the next and previous pointers. No elements must be moved. Insertions and deletions in the middle of vectors and lists and at the beginning of vectors require other elements to be moved. This is why they require linear time.

In addition to the containers list above, ordinary arrays and the string class can be considered sequence containers. As will be shown later, they can also be used with the generic algorithms that provide much of the usefulness of the STL. In addition, most

STL implementations also provide stack, queue and priority queue container classes. These classes are created using container adapters and one of the core STL sequence containers. We explore this in a later lesson.

Here's a example that shows some of the usefulness of the STL. Containers, combined with iterators and the generic algorithms offer simple solutions to many common programming examples. For now, don't worry if you don't understand everything. We'll cover everything in detail in later lessons. For now, here are the main points to notice.
Notice how easy it is to create and load a vector. The default constructor creates an empty vector. Push_back() adds elements to the end of the vector. The vector was sorted using one simple call to the generic algorithm sort. The vector was reversed using one simple call to the generic algorithm reverse. Sort() and reverse() take iterators as arguments. The vector class method begin() returns an iterator to the start of the vector; end() returns an iterator one past the end of the vector.

#include <vector> #include <iostream> #include <algorithm> using namespace std; int main( ) { // Creates an empty vector vector <int> v1; // size_type is defined in the vector include // file as the type return by the size method // Think of it as a really fancy way to declare // an int. vector <int>::size_type i; // Add elements to the end of v1 v1.push_back(5); v1.push_back(12); v1.push_back(1); v1.push_back(392); // See what we got for (i = 0; i < v1.size(); i++) { cout << v1[i] << " "; } cout << endl; // Sort sort(v1.begin(),v1.end()); // See what we got for (i = 0; i < v1.size(); i++) { cout << v1[i] << " "; } cout << endl; // Reverse reverse(v1.begin(),v1.end()); // See what we got for (i = 0; i < v1.size(); i++) { cout << v1[i] << " "; } cout << endl; return 0; }

Results: 5 12 1 392 1 5 12 392 392 12 5 1

The second type of container in the STL is an associative container. These containers support the construction of hashes and sets. A set is a collection of keys. It can be used to determine whether a particular object is present or not. A hash is a collection of key,value pairs. Each value may be inserted by key and located by its key. All of the associative containers in the STL are sorted. That is, based on key, as elements are inserted, they are stored sorted. This allows fast retrieval. There are four associative containers that are part of the STL. These classes are implemented as templates in the library. set <key_type> - The set container holds unique keys of any type. It provides for fast retrieval of keys and tests for presence of particular keys. multiset <key_type> - The multiset contain also holds keys of any type, but they need not be unique. Multiple copies of each key may be stored. Moults also provides for fast key retrieval. map <key_type, value_type> - A map is a hash. It stores objects of one type, the values, in a sorted order based on objects of another type, the keys. Maps are also optimized for fast retrieval of values by key. multi map <key_type, value_type> - Allows storage of values based on keys. The keys may be duplicated.

Here is a simple example using a set. Again, don't worry if you don't understand every line. Everything will become clear as you proceed through the tutorial. Just try to follow the key points. Notice how easy it is to create a set. In this case, the keys of the set are strings. Notice how easy it is to add keys to the set The find function was used to search for a particular key. It is a member function of set. It took only 21 lines of code to determine that I am not a Simpson. :-)

#include <iostream> #include <string> #include <set> using namespace std; int main( ) { // Creates an empty set set <string> simpsons; // Add keys to set simpsons.insert("Homer"); simpsons.insert("Marge"); simpsons.insert("Bart"); simpsons.insert("Lisa"); simpsons.insert("Maggie"); // find returns an iterator to an element if found // or an iterator to one past the end if not. set <string>::iterator iter = simpsons.find("John"); if (iter != simpsons.end()) { cout << "Doh!!!" << endl; } else { cout << "Not a Simpson." << endl; } return 0; }

So far, we've seen the container classes and in the examples, we've see glimpses of the generic algorithms. One of the key points in learning to use the STL is understanding how iterators are used to link the container classes to the generic algorithms. Each of the algorithms takes iterators as at least some of its arguments and each of the containers has methods to that provide iterators. To understand iterators, some understanding of pointers is useful. Lesson 8 of the C++ Tutorial covers pointers. Iterators are really just special classes that implement pointer like operations on the STL container classes. They are an abstraction of the simple pointer. Let's think about pointers first. A pointer holds an address and we can dereference a pointer to get or set the value at that address.
int a[10] = {1,2,2,3,6,7,8,10,3,12}; int *pt = &a[0]; //Pointer pt hold the address of the first element of the array cout << *pt; // writes "1" to standard out; *pt = 5; // assigns 5 to the first element of the array.

Pointers can be checked for equality or inequality.


int *pt2 = &a[2]; if (pt == pt2) { //Do something }

Pointers can be incremented and decremented.


pt++; ++pt; pt--; --pt;

Pointer arithmetic can be performed to access different locations within our array, or within memory. Operations such as +,+=,-,-= and relational operations such as <, <=, >, >= are supported
int a[10] = {1,2,2,3,6,7,8,10,3,12}; int *pt = &a[0]; //Pointer pt hold the address of the first element of the array pt = pt + 5; *pt = 5; // assigns 5 to the sixth element of the array.

As we will see, the STL provides several types of iterators. Some support all of the functionality described above, some support only part. As mentioned on the previous page, the STL provides several types of iterators. Some containers support only certain types of iterators. Each of the algorithms requires certain types of iterators. By matching a container providing the correct type of iterator with the iterator requirements of an algorithm, efficiency in the use of the library is maintained. Let's look at the different types of iterators. Input Iterator - This iterator is read-only. That is it cannot be assigned to. It must support *, ==, =- and ++ operations.

Output Iterator - This iterator is write-only. It cannot be read. It must support * and ++ operators. Forward Iterator - Read/Write iterator. It must support *, ==, =- and ++ operations. Bidirectional Iterator - Read/Write iterator. It must support *, ==, =-, ++ and -- operations. Random Access Iterator - Read/Write iterator. It must support *, ==, =-, ++, --, +, +=, -, -=, <, <=, > and >= operations.

Note that the iterators are actually part of an inheritance hierarchy. A random access iterator is a bidirectional iterator. A bidirectional iterator is a forward iterator. Now, let's look at a simple example that shows how iterator requirements of the algorithms and the iterator capabilities of the containers shape the choice of both container and algorithm in our code. Let's assume that we have a sequence of numbers that must be stored and sorted. The prototype of the sort algorithm is as follows.
template<class RandomAccessIterator> void sort(RandomAccessIterator _First, RandomAccessIterator _Last); Sort is implement as a template and it requires a random access iterator. To use sort we must choose a container that provides random access iterators. Vector and deque satisfy this requirement, but list does not. List only provides bidirectional iterators, as a result of its underlying doubly linked-list implementation. You cannot randomly access elements in a linked list. You must traverse the list to get to a particular element. Again, this matching of containers and algorithms helps to maintain efficiency.

Many of the generic algorithms on the previous page perform some type of comparison or test as they execute. For instance, the count function returns the number of elements equal to some value. The find function returns an iterator containing the location of the first element equal to some value. If this were the limit to the flexibility of these algorithms they would still be useful, but the STL was designed to make the algorithms significantly more flexible and more useful. Many of the algorithms have versions that take a function or function object as an parameter. Function objects are classes that have the function operator, (), overloaded. These function or function objects usually take one or two parameters and return a boolean, or true/false value. A function that returns a boolean value is also called a predicate. Let's see how a predicate can be used to extend the functionality of the count algorithm.
#include <iostream> #include <vector> #include <algorithm> using namespace std; bool less (const int i) { return i < 25; } bool more (const int i) { return i > 25; } int main( ) { // Creates an empty vector vector<int> v1; // Add numbers to vector v1.push_back(12); v1.push_back(25); v1.push_back(25); v1.push_back(35); v1.push_back(45); // Finds the number of elements equal to 25 int quantity = count(v1.begin(), v1.end(), 25);

cout << quantity << " elements equal to 25" << endl; // Finds the number of elements less than 25 quantity = count_if(v1.begin(), v1.end(), less); cout << quantity << " elements less than 25" << endl; // Finds the number of elements greater than 25 quantity = count_if(v1.begin(), v1.end(), more); cout << quantity << " elements greater than 25" << endl; return 0; }

OUTPUT: 2 elements equal to 25 1 elements less than 25 2 elements greater than 23 Notice how the behavior of count is modified when a predicate, such as the functions less or more, is provided as a parameter. Instead of a simple function as shown in this example, a class that overloads the function operator, (), could also be used. We will see the benefits of this in a later lesson. Also, please note that the STL provides built-in function objects via the functional include file. This will be covered in a later lesson. The last piece of the STL to be introduced in this overview are adapters. Adapters modify the interface or the behavior of other STL components. Adapters can be used to modify iterators, containers or function objects. For example, an iterator can be modified into a reverse iterator that traverses a container in the opposite direction. A vector can be modified into a stack using a container adapter. The behavior and interface of function objects can be changed by function adapters. Let's redo the last example using two binary predicates that are part of the STL, less and greater. Less compares two values and returns true if the first is less than the second. Greater returns true is the second is greater than the first. On the last page we counted values in a vector less that 25 and greater than 25. We want to use these predicates with the "second" value hard coded to 25. To do this using the STL provided less and greater function objects we need to use an adapter, bind2nd, that binds the second value in these function objects to 25. For now, you only need to see how adapters can greatly extend the usefulness of other STL components. We'll handle all the details in later lessons.
#include <iostream> #include <vector> #include <algorithm> #include <functional> using namespace std; int main( ) { // Creates an empty vector vector<int> v1; // Add numbers to vector v1.push_back(12); v1.push_back(25); v1.push_back(25); v1.push_back(35); v1.push_back(45); // Finds the number of elements equal to 25 int quantity = count(v1.begin(), v1.end(), 25); cout << quantity << " elements equal to 25" << endl;

// Finds the number of elements less than 25 quantity = count_if(v1.begin(), v1.end(), bind2nd(less<int>(),25)); cout << quantity << " elements less than 25" << endl; // Finds the number of elements greater than 25 quantity = count_if(v1.begin(), v1.end(), bind2nd(greater<int>(),25)); cout << quantity << " elements greater than 25" << endl; return 0; }

OUTPUT: 2 elements equal to 25 1 elements less than 25 2 elements greater than 23

You might also like