Lab 8: Containers

Vectors, Lists, Queues, and iterators

Most programs revolve around managing data, and the container you choose often shapes performance more than the algorithm you run.

In this lab we will tour cornerstones of the C++ Standard Template Library: vector, list, queue, and map. We will compare how each container grows, accesses, and orders its elements. Lastly, we will understand the common tool for traversing all containers: iterators.

§1 Vector #

A std::vector in C++ is a dynamic array that can grow in size as needed. It offers several member functions and operators to manipulate data.

Vectors are template objects, meaning that you need to specify the type of data that is contained in the vector up front. A vector can be declared to hold any type of data, including user-defined data types such as structs.

vector<string> phrases;

Important Vector Member Functions:

MethodDescription
push_back(value)Adds an element at the end of the vector.
insert(iterator position, value)Inserts an element at the given position.
pop_back()Removes the last element.
erase(iterator position)Removes an element from the given position.
size()Returns the number of elements in the vector.
empty()Returns true if the vector is empty.
clear()Removes all elements from the vector.
at(index)Accesses the element at the given index with bounds checking.
capacity()Returns the number of elements that can be stored without reallocating memory.

Source: mycplus.com

Example of Using a Vector:

#include <iostream>
#include <vector>  // import the vector library

int main() {
  std::vector<int> numbers; // declare a vector that holds integers

  // Add some elements
  numbers.push_back(10);
  numbers.push_back(20);
  numbers.push_back(30);

  // Access first element using .at(0), similar to accessing an array with arr[0]
  std::cout << "First element: " << numbers.at(0) << std::endl;

  // Delete last element from the vector
  numbers.pop_back();

  // Insert and erase elements in the middle of the vector
  // these methods use an iterator, which will be covered later in this lab
  numbers.insert(numbers.begin(), 5); // add a 5 at the front of the vector
  numbers.erase(numbers.begin() + 1); // delete the second item

  // Get the number of elements in the vector
  std::cout << "Size of vector: " << numbers.size() << std::endl; // Size of vector: 3
}

Vector Operators:

std::vector<int> vec1 = {11, 12, 13};
std::vector<int> vec2;
vec2 = vec1;  // vec2 is now a copy of vec1
std::cout << vec2[1]; // prints:  "12"

§1.1 Vector Comparison #

When comparing vectors, the items are compared element by element, similar to how strings are compared. For vector1 > vector2, the comparison proceeds as follows:

  1. The first elements of both vectors are compared.
    • If vector1[0] > vector2[0], then vector1 > vector2 is true.
    • If vector1[0] < vector2[0], then vector1 > vector2 is false.
    • If vector1[0] == vector2[0], the comparison moves on to the next element.
  2. This process continues element by element until a difference is found.
  3. If all compared elements are equal, but one vector is longer, the longer vector is considered greater. For example, if vector1 has more elements than vector2, and all compared elements are equal, then vector1 > vector2 is true.

Example:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> vector1 = {1, 2, 3};
  std::vector<int> vector2 = {1, 2, 2};

  if (vector1 > vector2) {
    std::cout << "vector1 is greater than vector2" << std::endl;
  } else {
    std::cout << "vector1 is not greater than vector2" << std::endl;
  }
}

In this example, vector1 > vector2 will be true because the third element of vector1 (3) is greater than the third element of vector2 (2).

§1.2 Vector Trade-Offs #

Pros:

Cons:

§1.3 Vector Documentation #

Full documentation on std::vector:

§2 List #

An std::list is a doubly linked list that allows fast insertion and deletion at both ends. Unlike vectors, std::list does not provide arbitrary access to items in the middle of the list.

A C++ list is implemented as a “chain” of individual “nodes.” Each node contains two main components: the data being stored, and a pointer to the next node in the chain. In a doubly linked list, like std::list, each node also holds a pointer to the previous node, allowing traversal in both directions. This structure allows for efficient insertions and deletions at any point in the list but requires sequential access to reach specific elements.

Like vectors, Lists are also template objects, meaning that you need to specify the type of data that is contained in the list up front. A list can be declared to hold any type of data, including user-defined data types such as structs.

list<string> phrases;

Important list Member Functions

MethodDescription
push_back(value)Adds an element at the end of the list.
push_front(value)Adds an element at the beginning.
front()Returns the first element.
back()Returns the last element.
pop_back()Deletes the last element.
pop_front()Deletes the first element.
size()Returns the number of elements in the list.
insert(iterator position, value)Inserts an element at the specified position.
erase(iterator position)Removes the element at the specified position.
clear()Removes all elements from the list.
empty()Checks if the list is empty.

Notice there is no function to immediately access elements in the middle of the list. This can only be accomplished by using an iterator.

Example of Using a List:

#include <iostream>
#include <list>

int main() {
  std::list<int> numbers; // declare a list that holds integers

  // Add elements to the list
  numbers.push_back(10);
  numbers.push_back(20);
  numbers.push_front(5); // Add at the front of the list
  // list contents: 5, 10, 20

  // Print first and last item in list
  std::cout << "First item: " << numbers.front() << std::endl; // First item: 5
  std::cout << "Last item: " << numbers.back() << std::endl; // Last item: 20

  // Delete the last element
  numbers.pop_back();

  // Display the size of the list
  std::cout << "Size of list: " << numbers.size() << std::endl; // Size of list: 2

  // list iterators can only be incremented by 1 at a time
  numbers.insert(numbers.begin()++, 55);  // Insert 55 as the second element
}

List Operators:

§2.1 List Comparison #

List comparison works in the same way as vector and string comparisons, in that each element is compared one at a time.

Equality (==):

Example:

std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {1, 2, 3};
if (list1 == list2) {
  std::cout << "Lists are equal\n";
}

In this example, list1 == list2 will be true because all elements are equivalent.

§2.2 List Trade-Offs #

Pros:

Cons:

§2.3 List Documentation #

Full documentation on std::list:

§3 Map #

An std::map in C++ is an associative container that stores elements in key-value pairs, where each key is unique. The map maintains its elements in a sorted order based on the keys, and it provides fast lookups, insertions, and deletions.

Maps are also template objects, meaning you need to specify the types of both the keys and values. Maps can be declared to hold any combination of data types, including user-defined types for both the key and value.

std::map<int, std::string> students;

Source: programiz.com

Important Map Member Functions:

MethodDescription
insert(pair{key, value})Inserts a key-value pair into the map.
erase(key)Removes the element with the specified key.
find(key)Returns an iterator to the element with the specified key, or map.end() if the key is not found.
at(key)Returns the value associated with the specified key.
Throws an exception if the key is not found.
operator[key]Accesses the value associated with the key.
If the key does not exist, a new key-value pair is created. myMap[123] = "name"
size()Returns the number of elements in the map.
empty()Returns true if the map is empty.
clear()Removes all elements from the map.

Example of Using a Map:

#include <iostream>
#include <map>  // import the map library

int main() {
  std::map<int, std::string> employeeNames;

  // Add some elements
  employeeNames[101] = "Alice";
  employeeNames[123] = "Bob";
  employeeNames[103] = "Charlie";

  // Access element using key
  std::cout << "Employee 101: " << employeeNames.at(101) << std::endl;

  // Remove an element
  employeeNames.erase(103);

  // Insert using the insert function
  employeeNames.insert({104, "David"});

  // Check if a key exists
  if (employeeNames.find(105) == employeeNames.end()) {
    std::cout << "Employee 105 not found." << std::endl;
  }

  // Get the number of elements in the map
  std::cout << "Number of employees: " << employeeNames.size() << std::endl;
}

Map Operators:

std::map<int, std::string> map1 = {{101, "Alice"}, {102, "Bob"}};
std::map<int, std::string> map2;
map2 = map1;  // map2 is now a copy of map1
std::cout << map2[102]; // prints: "Bob"

§3.1 Map Comparison #

Maps can be compared using relational operators, which compare the keys and values lexicographically. For map1 > map2, the comparison proceeds as follows:

  1. The first keys in both maps are compared.
    • If map1.first > map2.first, then map1 > map2 is true.
    • If map1.first < map2.first, then map1 > map2 is false.
    • If the first keys are equal, the comparison moves on to the values.
  2. This process continues key-value pair by key-value pair until a difference is found.
  3. If all compared elements are equal, but one map has more elements, the larger map is considered greater.

Map comparisons work in the same way as list and vector and string comparisons, in that each element is compared one at a time.

Equality (==):

Example:

#include <iostream>
#include <map>

int main() {
  // Create two maps with different key-value pairs
  std::map<int, std::string> map1 = {{1, "Alice"}, {2, "Bob"}, {3, "Charlie"}};
  std::map<int, std::string> map2 = {{1, "Alice"}, {2, "Bob"}, {3, "Dave"}};

  // Compare map1 and map2 for equality
  if (map1 == map2) {
    std::cout << "map1 is equal to map2" << std::endl;
  } else {
    std::cout << "map1 is not equal to map2" << std::endl;
  }
  // since the two maps are not identical, this will print: map1 is not equal to map2
}

In this example, map1 == map2 will be false because the third value of map1 (“Charlie”) is different than the third value of map2 (“Dave”).

§3.2 Map Trade-Offs #

Pros:

Cons:

§3.3 Map Documentation #

Full documentation on std::map:

§4 When should I use a vector/list/array/map? #

Choosing the right container type in C++ is crucial for writing performant and maintainable code. Each container has its own strengths and is suited for specific use cases. Here’s a general guide to help you decide when to use each container:

Array
Use an Array When:

Array Characteristics:


Vector
Use a Vector When:

List
Use a List When:

List Characteristics:


Map
Use a Map When:

§5 Iterators #

You have seen iterators mentioned several times now. Iterators are special objects that point to elements in a container, they are used to traverse through the list of elements. Most STL containers (including vector, list, and map) support iterators.

Iterators act like a pointer that provides access to the elements of the container one at a time, while abstracting away the details of the underlying data structure. Because of this abstraction, iterators are used very similarly across each of the containers that support them.

Iterator Operators:

Example: Using Iterators with Vectors and Lists

#include <iostream>
#include <vector>
#include <list>

int main() {
  std::vector<int> vec = {1, 2, 3, 4, 5};
  std::list<int> lst = {10, 20, 30};

  // Using iterator for vector
    std::vector<int>::iterator it; // declare an iterator for an integer vector

  // it = vec.begin() sets the "it" variable to an iterator pointing to the first element
  // we can move the iterator to the next element with it++
  // and we can get the value of the element by dereferencing the iterator with *it
  for (it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";  // Dereference the iterator
  }
  std::cout << std::endl;

  // Using iterator for list
  // it = lit.begin() gets an iterator for the list's first item
  std::list<int>::iterator lit;
  for (lit = lst.begin(); lit != lst.end(); ++lit) {
    std::cout << *lit << " ";
  }
  std::cout << std::endl;
}

Iterators can also be used with more complicated containers such as maps. A map iterator references a pair (the key and the value), so accessing the elements in a map iterator requires using the pointer -> syntax:

Example: Using Iterators with Maps

#include <iostream>
#include <map>

int main() {
  // Create a map of employee IDs and names
  std::map<int, std::string> employeeNames;

  // Insert some elements into the map
  employeeNames[101] = "Alice";
  employeeNames[102] = "Bob";
  employeeNames[103] = "Charlie";
  employeeNames[104] = "David";

  // Declare an iterator for the map
  std::map<int, std::string>::iterator it;

  // Iterate through the map using the iterator
  for (it = employeeNames.begin(); it != employeeNames.end(); ++it) {
    // Access the key and value using the iterator
    // notice we use the pointer -> syntax to access the "first" and "second" elements
    // "first" points to the key, "second" points to the value
    std::cout << "Employee ID: " << it->first << ", Name: " << it->second << std::endl;
  }
}

§6 Further Reading #

The textbook has more information on STL containers and Iterators in
Chapter 17: The Standard Template Library

§7 Questions #

  1. Explain the difference between size() and capacity() in a std::vector.
  1. Why is it important to be aware of the capacity of a vector when performing operations like push_back()?
  1. When would you choose to use a std::list over a std::vector? Provide at least two scenarios where a list is more suitable.
  1. What are three ways you can access a value stored at a specific key in a std::map?
  1. Provide code (a 1 line snippet is fine) to check if a key exists or not in a std::map?
  1. What is the primary difference between an iterator and a pointer in C++?
  1. Iterators can become “Invalid” in certain cases, such as when adding and deleting items from a vector, what can you do to avoid invalid iterators? (Assume you must use an iterator)