16: STL

UC Irvine - Fall ‘22 - ICS 45C

Quick list of things I want to talk about:

  • Vector
  • Map
  • for-each loops
  • auto

Expanded notes:

Keeping track of a variable number of elements can be challenging, as you might’ve seen in our projects. You need to manage pointers, clean memory you don’t need anymore, allow multiple types to be used, and much more.

The Standard Templates Library (STL) provides a bunch of things pre-implemented, so you can simply include them. Among those things, there are containers that can take care of the hard part of these structures, and we can use them. We’ll take a look at two such containers that can be very helpful: vectors and maps.

vectors

The first thing we’ll see are vectors. A vector is a sequence container that can expand and shrink according to the number of elements you want to store.

Let’s see an example of how we can use them:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;

int main() {
  vector<int> values;
  values.push_back(1);
  values.push_back(8);
  values.push_back(-123);

  cout << "Numbers added: " << values.size() << '\n';
  cout << "Second number: " << values[1];

  return 0;
}

A few things to note here:

  1. we need to include the <vector> header now;
  2. vectors are inside the std namespace, so we add a new using std::vector; directive;
  3. a vector is a template, so we need to use <TYPE> when creating one (in this example, we use int);
  4. to add things to the end of the vector, we can use .push_back;
  5. we can find out how many elements are there with .size;
  6. we can access it just like a regular array with [].

So that seems promising! Let’s see if we can loop over all the elements in it:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;

int main() {
  vector<int> values;
  values.push_back(1);
  values.push_back(8);
  values.push_back(-123);

  for (unsigned long i=0; i < values.size(); i++) {
    cout << i << ": " << values[i] << '\n';
  }

  return 0;
}

We can do it just like a regular array, and since we have the .size method, it’s pretty self-contained, we don’t need defines or constants to tell us the size.

for-each loops

There’s a better way to loop over it, though. Whenever you use a container from STL, you can usually loop over it using a for-each loop. This is similar to for-loops in python, where your variable goes through all elements in the container.

Let’s use the previous example to do that:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;

int main() {
  vector<int> values;
  values.push_back(1);
  values.push_back(8);
  values.push_back(-123);

  for (int num : values) {
    cout << num << '\n';
  }

  return 0;
}

We use for (int num : values) in this case. Here, num will go through all the elements that are inside values, so we will print:

1
8
-123

which are the values num will hold.

maps

Another container that’s available in STL are maps. Maps create an association between a key and a value – if you’re coming from python, a dictionary is a type of a map. This association can be helpful when you want to map something to something (e.g., plate numbers to parking spots) and provide a faster membership check for keys.

Let’s see an example of how to use them:

#include <iostream>
#include <map>
#include <string>

using std::cout;
using std::map;
using std::string;

int main() {
  map<string, int> vowel_count;
  vowel_count["hello"] = 2;
  vowel_count["blue"] = 2;
  vowel_count["red"] = 1;
  vowel_count["rhythm"] = 0;

  cout << "Key-value pairs inside: " << vowel_count.size() << '\n';
  cout << "Vowels in 'blue': " << vowel_count["blue"] << '\n';

  return 0;
}

Few things to note here:

  1. we need to include the <map> header;
  2. maps are inside the std namespace, so we add a new using std::map; directive;
  3. a vector is a template that requires two types, the key-type and the value-type (in this example, we use string for keys and int for values);
  4. to add things to the map, we use mapName[key] = value;
  5. we can find out how many elements are there with .size.

Loop through elements

However, maps are not ordered! That means that, in our previous example, hello might not necessarily come before red in memory. So we can’t really loop over its indices with .size like we did for vectors, but we can use a for-each approach.

#include <iostream>
#include <map>
#include <string>

using std::cout;
using std::map;
using std::string;

int main() {
  map<string, int> vowel_count;
  vowel_count["hello"] = 2;
  vowel_count["blue"] = 2;
  vowel_count["red"] = 1;
  vowel_count["rhythm"] = 0;

  for (std::pair<std::string, int> p : vowel_count) {
    cout << p.first << " = " << p.second << "; ";
  }

  return 0;
}

But now, instead of having a single value, we have a key and a value. So we need to use a pair from STL to go over that map, and we can access the key with .first and the value with .second.

Membership check

Another thing that we usually do with maps is to check if a key is inside. To do that, you would use the .find method:

#include <iostream>
#include <map>
#include <string>

using std::cout;
using std::cin;
using std::map;
using std::string;

int main() {
  map<string, int> vowel_count;
  string user_input;

  vowel_count["hello"] = 2;
  vowel_count["blue"] = 2;
  vowel_count["red"] = 1;
  vowel_count["rhythm"] = 0;

  cout << "What key do you want to look for?\n";
  cin >> user_input;

  map<std::string, int>::iterator entry = vowel_count.find(user_input);

  if (entry == vowel_count.end()) { // end of the map, didn't find the key.
    cout << "Could not find '" << user_input << "' inside the map.\n";
  } else {
    // Found the entry, key is entry->first, value is entry->second.
    cout << "map[" << user_input << "] = " << entry->second << '\n';
  }

  return 0;
}

Here, the find method returns a map iterator, and then we can check if we reached the end of our map, which indicates that we couldn’t find it, or we have a different value, which means we do have the key in this map.

auto-typed variables

For the two last map examples, the types started getting a little confusing. It might be hard to remember to use a pair, an iterator, a const_iterator, etc. But you don’t really need to! C++ allows you to use an auto type, where you’re asking the compiler to infer what type this variable should be. Usually, when using STL containers, you will see a lot of auto variables!

Let’s combine our last two examples and see how auto would be helpful:

#include <iostream>
#include <map>
#include <string>

using std::cout;
using std::cin;
using std::map;
using std::string;

int main() {
  map<string, int> vowel_count;
  string user_input;

  vowel_count["hello"] = 2;
  vowel_count["blue"] = 2;
  vowel_count["red"] = 1;
  vowel_count["rhythm"] = 0;

  cout << "What key do you want to look for?\n";
  cin >> user_input;

  auto search = vowel_count.find(user_input);

  if (search == vowel_count.end()) { // end of the map, didn't find the key.
    cout << "Could not find '" << user_input << "' inside the map.\n";
  } else {
    // Found the entry, key is search->first, value is search->second.
    cout << "map[" << user_input << "] = " << search->second << '\n';
  }

  cout << "Complete map:\n";
  for (auto entry : vowel_count) {
    cout << entry.first << " = " << entry.second << "; ";
  }

  return 0;
}

Here we just used auto to store the return of find and for our loop variable. So in those cases, auto is very helpful; you don’t need to figure out if this needs to be an iterator, a pair, a const iterator, a const pair, and so on.

However, you cannot use auto all the time. We can do that because these are “inferrable” types, that is, the compiler knows that types they should be based on the function return type (for find) and the type of our container (for the loop). If you have some code like this:

#include <iostream>

using std::cin;

int main() {
  auto var;
  cin >> var;
  return var;
}

you should get an error that means the compiler doesn’t know the type this variable should be:

error: declaration of variable 'var' with deduced type 'auto' requires an initializer
  auto var;
       ^
1 error generated.

So in other words, auto is just an alias to a type we know what it is, it’s not a magic type!

Further reading

There are other containers that might be interesting:

And also some pre-implemented algorithms in STL:

References