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:
- we need to include the
<vector>
header now; - vectors are inside the
std
namespace, so we add a newusing std::vector;
directive; - a vector is a template, so we need to use
<TYPE>
when creating one (in this example, we useint
); - to add things to the end of the vector, we can use
.push_back
; - we can find out how many elements are there with
.size
; - 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 define
s 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:
- we need to include the
<map>
header; - maps are inside the
std
namespace, so we add a newusing std::map;
directive; - a vector is a template that requires two types, the key-type and the value-type (in this example, we use
string
for keys andint
for values); - to add things to the map, we use
mapName[key] = value
; - 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: