Containers and Iterators

This lecture covers a broad overview of containers as well as the fundamental approaches to implementing their underlying data structures.

Containers serve a variety of purposes. Here's a few containers from the C++ standard library as examples:

  • A std::vector<double> could store datapoints for statistical analysis
  • A std::set<string> could represent uniqnames of students registered for a course
  • A std::map<string, double> could allow us to look up the price of an item on a menu by providing its name

We'll also take a look at the way the standard library uses iterators as the abstraction for locating and traversing through elements.

Updated Fall 2026

1: Introduction to Standard Library Containers




2: Exceptions

We'll briefly cover a miscellaneous topic, exception handling. Some functions on containers (and other functions, generally) from the C++ standard library "throw" exceptions in cases of invalid input.

For example, consider hypothetical RMEs for the [] operator and .at() functions on a std::vector<T>:

// REQUIRES: The vector is not empty.
// EFFECTS: Returns (by reference) the element at the given index.
T & operator[](size_t index);

// EFFECTS: Returns (by reference) the element at the given index,
// unless the index is out of bounds, in which case a std::out_of_range
// exception is thrown.
T & at(size_t index);

The [] operator uses a REQUIRES clause to specify that the vector must not be empty, otherwise undefined behavior may occur. On the other hand, the .at() function uses an EFFECTS clause to specify that it will deterministically throw a std::out_of_range exception if the index is out of bounds.

So what does "throwing an exception" mean? First, an exception is an object that represents an error that has occurred. When the function throws the exception, it does not return in the normal way or yield a result, but instead begins an alternate control flow path to seek out appropriate error handling code.

Error handling code is specifed with a try/catch construct. The try block contains the code that may throw an exception, and the catch block contains the code that will handle the exception if it is thrown. The catch block also specifies the type of exception it can handle. If an exception is thrown, but not caught, the program terminates with an error message.

Here's an example:

#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
int main() {
  std::vector<int> vec = {1, 2, 3};
  try {
    int value = vec.at(3); // 3 is out of bounds
  }
  catch (const std::out_of_range &e) {
    cout << "Error - index was out of bounds." << endl;
  }
}

This also works across function calls, so if an exception were to be thrown in a helper function, it could be caught by the caller (or the caller's caller, and so on…).

In project 4, you'll use the csvstream library to read CSV (Comma Separated Values) input files, and you'll catch an exception thrown by the library if the input file cannot be opened. The project spec links to an example of how to do this.

We'll return to exceptions near the end of the course and discuss them in more detail.



3: Sequential Containers with Contiguous Allocation
3.1 Not Started

There are two fundamental approaches to data representation for containers. The first of these is to use a contiguous allocation (i.e. elements are stored in an underlying array). std::array and std::vector are sequential containers from the C++ standard library that use this approach.


3.1

Which of the following are true?



4: Sequential Containers with Linked Structures
4.1 Not Started 4.2 Not Started

The second fundamental approach to data representation for containers is to use a linked structure (i.e. elements are stored in nodes that point to each other). std::list and std::forward_list are sequential containers from the C++ standard library that use this approach.


4.1 Exercise: Time Complexity

Determine the time complexity of each of the following operations on the given data structure. Using a worst-case analysis, determine whether each function has O(1) constant time complexity or O(n) linear time complexity.

Updating the value of the first element in a std::array.

Inserting a new element at the front (left) of a std::vector.

Inserting a new element at the back (right) of a std::vector.

Printing the value of the first element in a std::list.

Inserting a new element at the front (left) of a std::list.

Printing the element in the middle of a std::list.

Assuming you already have a pointer to some node within a std::list, inserting a new element right after that position.


4.2 Exercise: Binary Search

Recall the binary search algorithm, where we find a desired value in a sorted array by:

  1. Finding the middle value current search range by indexing at the halfway point.
  2. Comparing the query value to the middle value.
  3. Repeating the process on the left or right half of the array, depending on the comparison result (or stop if we find the value).

Using binary search, a lookup operation in a sorted array can be implemented in worst-case logarithmic time, O(log n).

However, this isn't the case for a sorted linked list! Why not? Explain why binary search is not a good fit for a linked list.


Sample solution…

Binary search relies on efficient indexing to get the middle value in the array. In an array, this is possible via pointer arithmetic. But linked lists do not support efficient indexing! To take one step of the binary search algorithm, we'd need to traverse linearly through 50% of the list by following next pointers. This first step alone already puts us at linear time complexity (and rules out logarithmic time).



5: Iterators and Traversal by Iterator

The C++ standard library uses the concept of an iterator to indicate the position of an element in a list and to facilitate traversal through elements.


Note that traversal by iterator is analogous to traversal by pointer, which we've seen previously. (Although not all iterators are literally implemented as pointers.)



6: The auto Keyword




7: Range-based for Loops




8: Iterator Interfaces

Iterator types are also used to specify the parameter or return types for many functions from the standard library.




9: Sets and Maps
9.1 Not Started

The other main category of containers are associative containers, which include sets and maps.


Let's take a look at std::map from the C++ standard library in more detail:


9.1 Exercise: Drive Thru with std::map

Copied below are the std::map interfaces for find, insert, and operator[]:

// EFFECTS: Returns an iterator to the entry in this map with the given
//           key, if one exists. Otherwise, returns an end Iterator.
iterator find(const Key_type& k) const;

// EFFECTS: Attempts to insert given element into this Map.
//          Case 1: Successful Insert
//           If the map does not already contain an entry for the
//           given key, creates a new entry with the given key/value
//           and returns an iterator to it, paired with true.
//          Case 2: Blocked Insert
//           If the map already contains an entry for the given key,
//           returns an iterator to the existing entry, paired with false.
std::pair<iterator, bool> insert(const std::pair<Key_type, Value_type> &val);

// EFFECTS: Returns a reference to the mapped value for the given key.
//          Case 1: Existing Entry
//           If the map contains an entry for the given key, returns a 
//           reference to its corresponding value.
//          Case 2: Auto-create Placeholder Entry
//           If the map does not already contain an entry for the given
//           key, inserts a new entry for it with a "value-initialized"
//           placeholder value. Returns a reference to that value.
//           Note: value-initialization for numeric types guarantees the
//           placeholder value will be 0 (rather than memory junk).
Value_type& operator[](const Key_type& k);

Consider the following class that allows looking up the price for a menu item at a drive-thru restaurant. Its internal data representation is a map where the keys are menu item names and the values are their corresponding prices.

class DriveThru {
public:
  // REQUIRES: Nothing
  // EFFECTS:  Returns the price for the item or -1
  //           if no such item exists.
  double getPrice(const string &item) const {
    // IMPLEMENTATION GOES HERE
  }

private:
  // A map from item names to their prices
  std::map<string, double> menu;
};

Which of the implementations of getPrice() below are correct? Write "correct" or "incorrect". For each that is not correct, explain what's wrong with it.

// Potential Implementation 1
double getPrice(const string &item) const {
  auto it = menu.find(item); 
  if (it != menu.end()) {
    return it->second;
  }
  else { return -1; }
}
// Potential Implementation 2
double getPrice(const string &item) const {
  auto it = menu.find(item); 
  if (it) {
    return it->second;
  }
  else { return -1; }
}
// Potential Implementation 3
double getPrice(const string &item) const {
  // If a real item exists, it will block this
  auto insert_result = menu.insert({item, -1});

  // Either the real price or the -1
  return insert_result.first->second;
}
// Potential Implementation 4
double getPrice(const string &item) const {
  if (menu.find(item) != menu.end()) {
    return menu[item];
  }
  else { return -1; }
}


Sample solution…

Implementation 1: Correct. The code obtains an iterator, checks against .end() from the same container, and returns the appropriate value accessed via it->second.

Implementation 2: Incorrect. An iterator cannot be implicitly converted to a boolean value. Instead, it must be specifically compared against an end iterator from the same container (obtained via calling .end()). The reason for this is that an iterator cannot necessarily tell on its own whether it is at the "one-past-the-end" position - for example, it may internally be implemented using a pointer, but would need to compare that pointer against the actual one-past-the-end address of the container.

Implementation 3: Incorrect. This implementation is pretty careless with the underlying representation - it's generally problematic to pollute the underlying map with the -1 values, and changing the map is also a violation of the const qualification on getPrice().

Implementation 4: Incorrect. There's some nuance to this one. At first glance, the algorithm seems fine, because we are only accessing the value if we have already determined that an entry is present for the given key. This would also prevent the case where a default placeholder 0 is added to the map for an absent key, since we avoid using [] in these cases. However, the compiler doesn't consider this control flow when determining whether we are allowed to call operator[] in a const-qualified context. Because operator[] isn't const-qualified, the compiler still prohibits calling it from getPrice(), which is const-qualified. The takeaway is that we really can't ever use [] on const maps (or in a context where they're treated as const).

You've reached the end of this lecture! Your work on any exercises will be saved if you re-open this page in the same web browser.

Participation Credit
Make sure to sign in to the page, complete each of the exercises, and double check the participation indicator at the top left of this page to ensure you've earned credit.