Modern C++ - STL

Financed from the financial support ELTE won from the Higher Education Restructuring Fund of the Hungarian Government.

17. The Standard Template Library

The Standard Template Library (STL) is part of the C++ standard. The STL is the most important example for generic programming.

Generic programming: make more abstract routines without loosing efficiency using parameterization (both data and algorithm).

Generics

Suppose we want to find an element in an array of integers:

 1 int t[] = { 1, 3, 5, ... };
 2 const int len =  t+sizeof(t)/sizeof(t[0]);
 3 
 4 // find the first occurance of a value
 5 int *pi = find( t, t+len, 55);
 6 
 7 if ( t+len != pi )
 8 {
 9   *pi = 56;
10 }

For this purpose we can define a very specific implementation. We walk throu the interval and check every elements until we find the value we are looking for, or we find the end of the interval.

Consider the arguments of the find function: the begin pointer specifies the beginning of the interval, but end points the place after the last element. Thi sway the empty interval can be expressed more easily.

 1 int *find( int *begin, int *end, int x)
 2 {
 3   while ( begin != end )
 4   {
 5     if ( *begin == x )
 6     {
 7       return begin;
 8     }
 9     ++begin;
10   }
11   return 0;
12 }

This function works, but too specific. It can be applied only for arrays of integers. We will generalize the function: we change it to accept more types, without compromising performance. Templates solve the problem:

 1 template <typename T>
 2 T *find( T *begin, T *end, const T& x)
 3 {
 4   while ( begin != end )
 5   {
 6     if ( *begin == x )
 7     {
 8       return begin;
 9     }
10     ++begin;
11   }
12   return 0;
13 }

This version of find works on array of doubles or even on strings. However, we are still restricted our algorithm to arrays.

Now we generalize on data structure. The idea is that on every data structure we can follow the same pseudo code:

  1. Take the first element (begin)
  2. Chack whether we reached the end of the interval ( begin != end )
  3. If not, check whether this is the element we are looking for ( *begin == x )
  4. If not, go to the next element of the interval ( ++begin )

What data structure dependent is how to reach the current element and how to go to the next element of the interval. To implement these concepts we introduce a new template type, the iterator, which is data structure dependent, and responsible to implement these operations.

 1 template <typename It, typename T>
 2 It find( It begin, It end, const T& x)
 3 {
 4   while ( begin != end )
 5   {
 6     if ( *begin == x )
 7     {
 8       return begin;
 9     }
10     ++begin;
11   }
12   return end;  // not 0
13 }

Notice, that we return either an iterator referring to the element we found or end, the iterator value referring outside of the range. It is more general and still easy to check criteria to inform the user that the searched element was not found.

An iterator is the abstraction of the “pointer” concept, however, implemented usually as a (nested) class in a usually templated container. Now, we can use our find function in a data structure independent way:

 1 // use with integer array
 2 int t[] = { 1, 3, 5, ... };
 3 const int len = t+sizeof(t)/sizeof(t[0]);
 4 
 5 int *pi = find( t, t+len, 55);
 6 
 7 if ( t+len != pi )
 8 {
 9   *pi = 56;
10 }
11 
12 // use with vector of doubles 
13 vector<double> v(t,t+len);  // initialize from int array
14 
15 vector<double>::iterator vi = find( v.begin(), v.end(), 55.0);
16 if ( v.end() != vi )
17 {
18   *vi = 56.66;
19 }
20 
21 // use with list of doubles
22 list<double> l(v.begin(),v.end()); // initialize from vector<double>
23 
24 list<double>::iterator li = find( l.begin(), l.end(), 55.55);
25 
26 if ( l.end() != li )
27 {
28   *li = 56.66;
29 }

Iterators are also have const version: const_iterator. The const_iterator behaves like a pointer to const, it is not a contant itself, but the referred memory is inmutable. Do not mix const iterator with const_iterator.

1 const vector<int> cv = {1,2,3,4,5};  // initialization, C++11
2 
3 vectort<int>::const_iterator cvi = find( cv.begin(), cv.end(), 55.55);
4 
5 if ( cv.end() != cvi )
6 {
7   // *cvi = 56;  // Syntax error, the referred memory is immutable
8   cout << *cvi;  // ok, read
9 }

In C++11, we have a shortcut to declare iterators using automatic type deduction.

1 vector<int> v = {1,2,3,4,5};              // initialization, C++11
2 const list<double> cl(v.begin(), v.end()) // initialization  
3 
4 // vector<int>::iterator
5 auto  vi = find(  v.begin(),  v.end(), 55);    
6 
7 // list<double>::const_iterator
8 auto cli = find( cl.begin(), cl.end(), 55.55);

Functors

When we want to execute more complex algorithms, we get some problems. Suppose, we want to find the third occurance of an element smaller than 55. This is not trivial with the original find algorithm.

There is an other find version: find_if which searches not for a concrete element, but the first element where a predicate is true:

 1 template <typename It, typename Pred>
 2 It find_if( It begin, It end, Pred p)
 3 {
 4   while ( begin != end )
 5   {
 6     if ( p(*begin) )
 7     {
 8       return begin;
 9     }
10     ++begin;
11   }
12   return end;
13 }

To use find_if we first have to define a predicate, a function taking one parameter and return true when this parameter satisfy the required criteria.

The most simple (but far from perfect) implementation for the predicate is a simple function, which returns true when called the third time with an argument less than 55.

1 // Pred1: not too good
2 bool less55_3rd( int x)
3 {
4   static int cnt = 0;
5   if ( x < 55 )
6     ++cnt;
7   return 3 == cnt;
8 }

It seems working when we call first:

1 vector<int> v = { 3, 99, 56, 44, 2, 8, 1, 5, 88, 6, 9};
2 auto i = find_if( v.begin(), v.end(), less55_3rd);
3 // *i == 2

But in real situations this is a wrong implementation:

1 vector<int> v = { 3, 99, 56, 44, 2, 8, 1, 5, 88, 6, 9};
2 auto i = find_if( v.begin(), v.end(), less55_3rd);
3 // *i == 2
4 i = find_if( v.begin(), v.end(), less55_3rd);
5 // i == v.end()

The problem is, that less55_3rd uses a local static variable cnt for counting, for functions this is the only feasible solution to remember the previous value of a local variable. Unfortunately, a static variable will keep its value between the separate calls of find_if too.

A further problem of static variables – locals or globals – is that they are also shared between parallel execution threads in a possible multithreaded application.

We need a solution in which we can allocate a separate memory for each applications of the function, separated from all – concurrent, or different in in time – applications of the same function. Classes do exactly this: data members provide separate memory space for each objects, while member functions provide the functionality.

 1 struct less55_3rd
 2 {
 3   less55_3rd() : cnt(0) { }
 4   bool operator()(int x)
 5   {
 6     if ( x < 55 )
 7       ++cnt;
 8     return 3 == cnt;
 9   }
10 private:
11   int cnt;
12 };

The constructor initializes the cnt data member which conts the hits. The function call operator provides the actual checks – called on every data items on the range by the find_if algorithm. For each application of less55_3rd we should create a new object. Instead of declare a variable with name, it is enough to create a temporary variable.

1 vector<int> v = { 3, 99, 56, 44, 2, 8, 1, 5, 88, 6, 9};
2 auto i = find_if( v.begin(), v.end(), less55_3rd);
3 // *i == 2
4 i = find_if( v.begin(), v.end(), less55_3rd);
5 // *i == 2
6 i = find_if( ++i, v.end(), less55_3rd);
7 // *i == 5
8 i = find_if( ++i, v.end(), less55_3rd);
9 // i == v.end()      // no more findings

Functors are flexible way to define arbitrary search, sort or other criteria by the user.

Remark: there is not always a good idea to create a functor with state. Some algorithms may copy or assign the functor parameter, therefore the state may changed differently as one suppose it.

One can generalize the less55_3rd functor to make it more generally usable. We can parameterize both the 55 and the 3 numeric parameters: instread of “wired-in” to the code they can be constructor parameters. Also we can generalize the types the functor works on.

 1 template <typename T>
 2 struct less_nth
 3 {
 4   less_nth( const T& t, int n) : t_(t), n_(n), cnt_(0) { }
 5   bool operator()(const T& t)
 6   {
 7     if ( t < t_ )
 8       ++cnt;
 9     return n_ == cnt;
10   }
11 private:
12   T   t_;
13   int n_;
14   int cnt_;
15 };
16 
17 vector<int> v = { ... };
18 auto i = find_if(v.begin(),v.end(),less_nth<int>(55,3));

Further generalization can be replace the operator< used in the function call operator with a functor parameter used to compare two elements with the less criteria.

Defining functors is a bit heavy weight solution since we have to write some boiler-plate code. The lambda functions can be used to replace functors with a much less syntactical overhead.