Introduction to the STL

Posted by Nick Gammon on Tue 01 Jul 2003 04:58 AM — 3 posts, 17,616 views.

Australia Forum Administrator #0
I have been playing with the Standard Template Library (which I will refer to as "STL" from now on, rather than "the STL") and am making some notes now for my own benefit, and any other C++ programmers that are interested in using STL in their programs.




What is STL?

STL is a collection of useful algorithms which are implemented using the C++ "template" mechanism, which allows you to use them for virtually any "type" of data.

Containers

A fair bit of STL is based around the idea of containers - groups of related things - for examples lists, vectors, queues and so on. STL is designed to be consistent in its interface so that once you are used to one container you can pretty-much use them all.

Iterators

STL relies heavily on "iterators" which I will explain in a minute. Basically you use iterators to access elements in a container.

Elements

Containers contain things, which since STL uses templates, can be virtually any C++ type (eg. integer, string, pointer).

Algorithms

STL provides a whole lot of useful algorithms, like sort, merge, generate, find, replace and so on. Generally speaking the algorithms work with iterators, and can be applied to virtually any container. However some algorithms (like sort) need certain types of containers (in this case, one that can be randomly accessed).




Example 1

First, let's illustrate the idea of containers with some straight C++ ...


#include <iostream>

using namespace std;

int main (void)
  {
  // container
  int v [] = { 22, 33, 66, 102 };   

  // iterator
  int * i;               

  i = v;          // iterator is at start

  cout << *i << endl;    // display one element

  ++i;    // move to next element

  cout << *i << endl;    // display one element

  return 0;
  } // end of main


Output

22
33


This example shows an ordinary array as a container of numbers, and the iterator "i" (which is a pointer).

The important thing about the iterator is that you can increment it to move to the next element, and dereference it (*i) to get the element's contents.

Now let's do that the STL way ...



Example 2


#include <iostream>
#include <vector>

using namespace std;

int main (void)
  {
  // container
  vector<int> v;
  
  // add elements to the container
  v.push_back (22);
  v.push_back (33);
  v.push_back (66);
  v.push_back (102);

  // iterator
  vector<int>::iterator i;               

  i = v.begin ();          // iterator is at start

  cout << *i << endl;    // display one element

  ++i;    // move to next element

  cout << *i << endl;    // display one element

  return 0;
  } // end of main


Output

22
33


You are probably thinking the straight C way was easier, but the example is really to illustrate the concepts of iterators and containers.

Also, it shows how using STL adding 1 to an iterator moves onto the next element, however in a more general way. If that example was changed so that the vector became a list, or a queue, it would still work exactly the same way.

To simplify things, we can copy from a C array into a vector, using the "copy" algorithm, thus saving a bit of effort ...



Example 3


#include <iostream>
#include <vector>
#include <iterator>

using namespace std;

#define NUMITEMS(x) (sizeof(x) / sizeof(x[0]))

int main (void)
  {

  // C container
  int v1 [] = { 22, 33, 66, 102 };
 
  // STL container
  vector<int> v;

  // add elements to the container
  copy (v1, &v1 [ NUMITEMS (v1) ], back_inserter (v));

  // iterator
  copy (v.begin (), v.end (), ostream_iterator<int> (cout, " "));

  cout << endl;   // end of line

  return 0;
  } // end of main



Output

22 33 66 102


This example illustrates using "copy" to copy from one container to another. In this case the C array can be used as a container, and the vector as a second container. The "back_inserter" is needed to add elements to the end of the vector, otherwise the copy would copy beyond the end of the vector, and the program would crash.

The second "copy" uses an ostream_iterator (an output stream iterator) to copy to cout, thus simplifying displaying all the values.



Example 4



#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>

using namespace std;

template <class ForwardIterator>
void increment_fill (ForwardIterator first, int n)
  {
  int value = 0;
  for (int i = 0; i < n; i++)
    *first++ = value++;
  };

int main (void)
  {
 
  // STL container
  vector<int> v;

  increment_fill (back_inserter (v), 100);

  srand(time(NULL));
//  srand48(time(NULL));  // use on Linux

  // shuffle them
  random_shuffle (v.begin (), v.end ());

  // display results
  copy (v.begin (), v.end (), ostream_iterator<int> (cout, " "));

  cout << endl;   // end of line

  return 0;
  } // end of main



Output

4 19 57 41 36 30 27 87 18 24 66 16 34 69 83 23 97 55 91 15 33 1 31 3 98 2 37 95
90 49 32 48 52 54 96 59 10 8 70 42 71 38 5 94 25 72 84 89 39 73 64 11 12 40 45 92
85 78 86 62 99 79 67 53 46 7 6 56 77 74 75 35 88 26 93 17 14 82 0 58 65 29 28
63 43 9 60 47 13 51 80 50 61 76 22 81 68 20 21 44


This example illustrates using a user-written function to generate a range of numbers, and then randomly shuffle it with the STL random_shuffle algorithm.

I found the "srand" line made the numbers come out differently each time, without it they were always the same. However on Linux (Red Hat Linux 9) I needed the srand48 instead.


Amended on Wed 02 Jul 2003 02:19 AM by Nick Gammon
Australia Forum Administrator #1
Strings

STL includes a nice "string" class that lets you do all sorts of things with strings. However one thing it doesn't have built-in is find-and-replace. However it is easy to write your own. Here is an example ...



Example 5


#include <iostream>
#include <iterator>
#include <algorithm>
#include <string>

using namespace std;

// string find-and-replace
string FindAndReplace
  (const string& source, const string target, const string replacement)
  {
  string str = source;
  string::size_type pos = 0,   // where we are now
                    found;     // where the found data is

  if (target.size () > 0)   // searching for nothing will cause a loop
    {
    while ((found = str.find (target, pos)) != string::npos)
      {
      str.replace (found, target.size (), replacement);
      pos = found + replacement.size ();
      }
    }

  return str;
  };   // end of FindAndReplace

int main (void)
  {
 
  string s;

  s = "Nick Gammon is working today and not working tomorrow";
  
  cout << FindAndReplace (s, "working", "sleeping") << endl;

  return 0;
  } // end of main



Output

Nick Gammon is sleeping today and not sleeping tomorrow


Getting a bit fancier now, we'll try making a deck of cards and shuffling it...



Example 6


// disable warnings about long names
#ifdef WIN32
  #pragma warning( disable : 4786)
#endif

#include <iostream>
#include <sstream>
#include <iterator>
#include <algorithm>
#include <functional>
#include <string>
#include <vector>
#include <time.h>

using namespace std;

#define NUMITEMS(x) (sizeof(x) / sizeof(x[0]))

#define MAKE_STRING(msg) \
   (((ostringstream&) (ostringstream() << msg)).str())

// used for generate_n: adds 1 to a starting number 
// and returns a string with that number

class AddN : public unary_function<int, string>
  {
  int start;
  public:
  AddN (const int n) : start (n) {};  // constructor, initialises sequence
  string operator() () { return MAKE_STRING (start++); };
  };

int main (void)
  {
 
  // my vector of cards

  vector<string> cards;

  // honour cards have special names - use a C array

  string honours [] = { "Ace", "King", "Queen", "Jack" };

  // copy from C array to vector - back_insert adds to vector cards

  copy (honours, &honours [ NUMITEMS (honours) ], back_inserter (cards));

  // generate 9 other cards starting at 2

  generate_n (back_inserter (cards), 9, AddN (2));

  // add the word " hearts" to the end of each one

  transform (cards.begin (), cards.end (),
             cards.begin (),
             bind2nd (plus<string> (), " hearts"));

  // shuffle them

  srand(time(NULL));
//  srand48(time(NULL));  // use on Linux

  random_shuffle (cards.begin (), cards.end ());

  // display them

  copy (cards.begin (), cards.end (), 
        ostream_iterator<string> (cout, ", "));
  cout << endl;

  return 0;
  } // end of main



Output

9 hearts, 5 hearts, 3 hearts, 4 hearts, Queen hearts, Jack hearts, 7 hearts, 6 hearts, Ace hearts, King hearts, 10 hearts, 8 hearts, 2 hearts,


This example illustrates a few more things - using generate_n to generate a sequence (adding 1 each time) - this is similar in concept to the increment_fill in the earlier example (example 4) but showing how we can do the same thing by writing a function that is called by the generate_n algorithm.

In this case I wanted the numbers to be strings (so we could put names like hearts, clubs, etc. after them) so a slightly different approach was warranted.

The MAKE_STRING define I found on another website, it lets you transform arbitrary mixtures of numbers and text into a single string.

For example:


string mystr = MAKE_STRING (
    "hello " << boolalpha << true << 
    hex << 1234 << 
    octal << 3456 << 
    dec << 123456 << 
    scientific << 123.45 << 
    fixed << 1234.567 " );


The keywords "hex" "octal" etc. let you change the base for numbers.


The other really interesting thing in this example is the use of "transform" with "plus" to add the word "hearts" to the end of each item in the vector.

What is happening is that transform applies a transformation to each item in a sequence, in this case the transformation is plus<string> (ie. string concatenation) and the second argument is "hearts". To bind the argument "hearts" to the "plus" we use bind2nd.




To get a bit fancier again, we'll make a copy of the 13 cards and then change "hearts" to "diamonds". To do this we need to use the "insert" member function of vector, and then write a specialised Find_Replace unary function which can be called by transform. However to avoid hard-coding what we are finding and replacing, we will make a special unary function "Find_Replace". When it is instantiated its constructor is passed what to find, and what to replace it with. It saves those in internal variables.

Then the transform function calls the operator() once for each element in the array, effectively doing the find/replace for us on each element. Changes in bold.



Example 7


// disable warnings about long names
#ifdef WIN32
  #pragma warning( disable : 4786)
#endif

#include <iostream>
#include <sstream>
#include <iterator>
#include <algorithm>
#include <functional>
#include <string>
#include <vector>
#include <time.h>

using namespace std;

#define NUMITEMS(x) (sizeof(x) / sizeof(x[0]))

#define MAKE_STRING(msg) \
   (((ostringstream&) (ostringstream() << msg)).str())

// used for generate_n: adds 1 to a starting number 
// and returns a string with that number

class AddN : public unary_function<int, string>
  {
  int start;
  public:
  AddN (const int n) : start (n) {};  // constructor, initialises sequence
  string operator() () { return MAKE_STRING (start++); };
  };


// string find-and-replace
inline string FindAndReplace
  (const string& source, const string target, const string replacement)
  {
  string str = source;
  string::size_type pos = 0,   // where we are now
                    found;     // where the found data is

  if (target.size () > 0)   // searching for nothing will cause a loop
    {
    while ((found = str.find (target, pos)) != string::npos)
      {
      str.replace (found, target.size (), replacement);
      pos = found + replacement.size ();
      }
    }

  return str;
  };   // end of FindAndReplace

// a functor that will find and replace

class Find_Replace : public unary_function<string, string>
  {
  const string target;
  const string replacement;
  public:
  Find_Replace (const string tar, const string repl)
    : target (tar), replacement (repl) {};  // constructor
  string operator() (const string & str)
    { return FindAndReplace (str, target, replacement); };
  };


int main (void)
  {
 
  // my vector of cards

  vector<string> cards;

  // honour cards have special names - use a C array

  string honours [] = { "Ace", "King", "Queen", "Jack" };

  // copy from C array to vector - back_insert adds to vector cards

  copy (honours, &honours [ NUMITEMS (honours) ], back_inserter (cards));

  // generate 9 other cards starting at 2

  generate_n (back_inserter (cards), 9, AddN (2));

  // add the word " hearts" to the end of each one

  transform (cards.begin (), cards.end (),
             cards.begin (),
             bind2nd (plus<string> (), " hearts"));


  // make a copy at the end

  cards.insert (cards.end (), cards.begin (), cards.end ());
  
  // convert the word "hearts" to "diamonds"

  transform (cards.begin (), cards.begin () + 13,
             cards.begin (),
             Find_Replace ("hearts", "diamonds"));



  // shuffle them

  srand(time(NULL));
//  srand48(time(NULL));  // use on Linux

  random_shuffle (cards.begin (), cards.end ());

  // display them

  copy (cards.begin (), cards.end (), 
        ostream_iterator<string> (cout, ", "));
  cout << endl;

  return 0;
  } // end of main



Output

10 hearts, 9 diamonds, Queen diamonds, 6 hearts, 3 diamonds, 2 diamonds, 4 hearts, 2 hearts, Jack diamonds, Ace hearts, 8 diamonds, King diamonds, Jack hearts, Ace diamonds, 10 diamonds, 8 hearts, King hearts, 5 hearts, 4 diamonds, 7 diamonds, 9 hearts, 3 hearts, 5 diamonds, Queen hearts, 7 hearts, 6 diamonds,

Amended on Thu 08 Jul 2004 11:35 PM by Nick Gammon
Australia Forum Administrator #2
Case-independent compares

Standard STL does not give you case-independent matching, so we need to write a special function to achieve that. The example below is one approach ...



Example 8


#include <iostream>
#include <string>

using namespace std;

// case-independent (ci) equality binary function
// for use by ciStringEqual
template <class T>
struct ci_equal_to : public binary_function<T,T,bool> 
{
  bool operator() (const T& c1, const T& c2) const 
    { return tolower (c1) == tolower (c2); }
};

// case-independent (ci) string compare
// returns true if strings are EQUAL
bool ciStringEqual (string s1, string s2)
  {
  
  pair <string::const_iterator, 
        string::const_iterator> result =
    mismatch (s1.begin (), s1.end (),   // source range
              s2.begin (),              // comparison start
              ci_equal_to<unsigned char> ());  // comparison

  // match if both at end
  return result.first == s1.end () &&
         result.second == s2.end ();

  } // end of ciStringEqual 


int main (void)
  {
 
  if (ciStringEqual ("nick", "NICK"))
    cout << "match";
  else
    cout << "no match";

  cout << endl;

  return 0;
  } // end of main



Output

match


This was actually a bit tricky to get right. It uses the "mismatch" algorithm to find the first mismatch between two strings, passing down a custom compare function, which compares using "tolower" and thus gives case-insensitivity.





The next trick is to do a case-independent compare of a list (rather than a vector) - finding a string that matches using the "find_if" function. This involves writing a special compare function, that basically calls the case-independent compare used earlier. The special function is in bold. However it is possible to avoid using it by using "ptr_fun" to transform the original function into a form acceptable to the find_if algorithm.




Example 9


// disable warnings about long names
#ifdef WIN32
  #pragma warning( disable : 4786)
#endif

#include <iostream>
#include <string>
#include <list>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

// case-independent (ci) equality binary function
// for use by ciStringEqual
template <class T>
struct ci_equal_to : public binary_function<T,T,bool> 
{
  bool operator() (const T& c1, const T& c2) const 
    { return tolower (c1) == tolower (c2); }
};

// case-independent (ci) string compare
// returns true if strings are EQUAL
// also used by binary function fStringCompare
bool ciStringEqual (string s1, string s2)
  {
  
  pair <string::const_iterator, 
        string::const_iterator> result =
    mismatch (s1.begin (), s1.end (),   // source range
              s2.begin (),              // comparison start
              ci_equal_to<unsigned char> ());  // comparison

  // match if both at end
  return result.first == s1.end () &&
         result.second == s2.end ();

  } // end of ciStringEqual


// case-insensitive string comparison functor

struct fStringCompare : binary_function<string, string, bool>
  {
  bool operator() (const string & s1, const string & s2) const
    { return ciStringEqual (s1, s2); };
  }; // end of fStringCompare



int main (void)
  {

  list<string> s;

  s.push_back ("Hello");
  s.push_back ("World");
  s.push_back ("Nice");
  s.push_back ("Day");

  list<string>::iterator pos;

  // method 1 - use the ciStringEqual function directly

  pos = find_if (s.begin (), s.end (), 
        bind2nd (ptr_fun (ciStringEqual), "nice"));

  if (pos != s.end ())
    cout << "Found " << *pos << endl;
  else
    cout << "Not found." << endl;

  // method 2 - use the special compare function
  
  pos = find_if (s.begin (), s.end (), bind2nd (fStringCompare (), "day"));

  if (pos != s.end ())
    cout << "Found " << *pos << endl;
  else
    cout << "Not found." << endl;

  return 0;
  } // end of main


Output

Found Nice
Found Day


This example uses "bind2nd" to "bind" the word "nice" to the value returned by scanning the vector, so that the comparison function compares the current value to "nice" in each case.




Finally, I'll try to make a single function that can be used as a straight case-insensitive string comparison, and also be used in find_if, without needing to use ptr_fun. I'm not sure how useful this is, but here it is ...

Example 10


// disable warnings about long names
#ifdef WIN32
  #pragma warning( disable : 4786)
#endif

#include <iostream>
#include <string>
#include <list>
#include <list>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;



// case-independent (ci) string compare
// returns true if strings are EQUAL
struct ciStringEqual : binary_function<string, string, bool>
  {

  // case-independent (ci) equality binary function
  // for use by ciStringEqual
  template <class T>
  struct ci_equal_to : public binary_function<T,T,bool> 
    {
    bool operator() (const T& c1, const T& c2) const 
      { return tolower (c1) == tolower (c2); }
    };

  bool operator() (const string & s1, const string & s2) const
    {
  
    pair <string::const_iterator, 
          string::const_iterator> result =
      mismatch (s1.begin (), s1.end (),   // source range
                s2.begin (),              // comparison start
                ci_equal_to<unsigned char> ());  // comparison

    // match if both at end
    return result.first == s1.end () &&
           result.second == s2.end ();

    }
  }; // end of ciStringEqual


int main (void)
  {

  list<string> s;

  s.push_back ("Hello");
  s.push_back ("World");
  s.push_back ("Nice");
  s.push_back ("Day");

  list<string>::iterator pos;

  // scan list for a comparison

  pos = find_if (s.begin (), s.end (), bind2nd (ciStringEqual (), "nice"));

  if (pos != s.end ())
    cout << "Found " << *pos;
  else
    cout << "Not found.";
 
  cout << endl;

  // do a single string comparison
  // note extra set of parenthese

  if (ciStringEqual () ("nick", "NICK"))
    cout << "match";
  else
    cout << "no match";

  cout << endl;

  return 0;
  } // end of main




Output

Found Nice
match


The modified function is in bold. Note that to call it as a straight string compare you now need an extra set of brackets, because it is a function object that needs to be constructed, and then the comparison that it is doing is passed as in the second set of brackets.

Whilst this version might be a bit more confusing to look at, what I do like about it is the ci_equal_to function has moved inside the ciStringEqual structure, effectively encapsulating its operation to where it is needed.