Introduction to the STL Containers
Explore the Standard Template Library (STL) containers in C++, including dynamic arrays, fixed-size arrays, double-ended queues, and associative containers like maps and unordered_maps. Understand how STL containers manage memory safely, support dynamic sizing, and enforce data access rules, enabling you to write robust and maintainable code that focuses on logic rather than manual memory management.
Up to this point, we have relied primarily on fundamental types and raw arrays to store data. Although raw arrays are straightforward, they impose significant limitations: their size must be fixed at creation, they do not track the number of elements they contain, and they require manual memory management, which is error-prone.
To develop robust and maintainable C++ applications, we need abstractions that are flexible, safe, and self-managing. This need is addressed by the Standard Template Library (STL). The STL provides well-tested data structures that automatically manage memory allocation, resizing, and data organization. By using these abstractions, we can focus on program logic and problem-solving rather than low-level memory concerns.
STL and generic containers
The Standard Template Library (STL) is a collection of reusable C++ components that provide commonly used data structures and algorithms.
One of the key strengths of the STL is that its containers are designed to work with many different data types. Instead of having separate containers for integers, strings, or other objects, the same container can store any type we specify.
When we declare a container, we indicate the type of elements it will hold inside angle brackets < >. The compiler then ensures that the container works safely and correctly with that type.
For example, std::vector serves as a blueprint for a dynamic array:
std::vector<int>creates a dynamic array of integers.std::vector<std::string>creates a dynamic array of strings.
This idea, writing code once and using it with different data types, is called generic programming. We will explore how this works in detail later.
Why use STL containers?
We prefer STL containers over raw arrays for three main reasons:
Dynamic sizing: Unlike raw arrays, most STL containers (like
std::vector) can grow or shrink as needed at runtime. We don't need to know the exact number of elements in advance.RAII and safety: STL containers follow the RAII principle. When a container goes out of scope, it automatically destroys the elements it holds and releases the memory. We never need to call the destructor explicitly.
Rich interfaces: Containers come with built-in functions to check their size (
.size()), access elements safely (.at()), and modify content (.push_back()).
Sequence containers
Sequence containers store data in a strict linear order. The position of an element depends on when and where we insert it, not on its value.
std::vector
The std::vector is the workhorse of C++. It represents a dynamic array stored in contiguous memory. It is almost always the best default choice because it offers fast access to elements and efficient iteration.
For most learners, std::vector is the first C++ STL container to understand because it combines dynamic sizing with safe memory management.
Let’s understand this step by step:
Line 6: We initialize a vector with three elements. The compiler deduces the type is
intbut explicitstd::vector<int>is preferred for clarity.Lines 9–10:
push_backadds elements to the end. The vector handles all memory reallocation internally.Line 14:
.size()returns the current number of elements (5).Line 17: Unlike
[],.at()checks if the index is valid, preventing dangerous memory access errors like in the code below.
std::array
If we know the size of our data at compile-time and it will never change, we use std::array. It is a thin wrapper around a raw array, but provides a safer, modern interface (for example, it knows its own size and supports bounds-checked access with .at()).
A std::array has a fixed size that cannot be changed after creation. However, its elements can be assigned or modified using [], .at(), or loops. It does not support adding or removing elements. It does not use dynamic memory and typically lives on the stack.
Let’s understand this step by step:
Line 6: The values inside
< >specify the element type and the size of the array. The size must be known at compile-time and cannot change.Line 8: Even though it is fixed, it provides standard STL functions like
.size()for consistency with other containers.
std::deque
The std::deque (pronounced "deck") stands for double-ended queue. While vectors are optimized for adding data to the end, deques are optimized for adding and removing data efficiently from both the beginning and the end.
To support this, std::deque provides two important methods:
push_back(value)→ inserts an element at the end.push_front(value)→ inserts an element at the beginning.
Both operations run in constant time and do not require shifting existing elements.
Let’s understand what happens here:
Line 11:
push_frontis the key feature here. Doing this with astd::vectoris extremely slow because all existing elements must shift over. Withstd::deque, it is instant.
Associative containers
Not all STL containers store data by position. Sequence containers such as std::vector, std::array, and std::deque organize elements in a linear order. Sometimes, however, we want to store data in a way that lets us look up values directly using a key.
This is where associative containers become useful. Instead of accessing elements by numeric index, we access them by a key. A common example is storing a student ID with a name, or a word with the number of times it appears.
Two common associative containers are std::map and std::unordered_map.
std::mapstores key-value pairs in sorted key order.std::unordered_mapstores key-value pairs using hashing, which gives very fast average-case lookup.
std::unordered_map
The std::unordered_map is the closest STL container to what we usually call a hash map. It stores data as key-value pairs and allows us to retrieve a value directly from its key. This makes it useful when fast lookup is more important than keeping data sorted.
Let’s understand this step by step:
Line 6: We declare an
unordered_mapwithintkeys andstd::stringvalues.Lines 8–10: We insert key-value pairs into the container. Here, student IDs act as keys and names act as values.
Line 12: We access a value directly using its key.
Lines 14–16: We use
.find()to check whether a key exists. If the key is not found,.find()returns.end().Line 18: We remove a key-value pair using
.erase().Lines 20–22: We iterate through all stored key-value pairs.
std::map
The std::map also stores data as key-value pairs, but it keeps the keys sorted automatically. This makes it useful when we want to process elements in order.
Let’s understand this step by step:
Line 6: We declare a
mapwithstd::stringkeys andintvalues.Lines 8–10: We insert key-value pairs into the container.
Line 12: We access a value using its key.
Lines 14–16: We iterate through the map. The elements appear in sorted key order.
std::map versus std::unordered_map
Both containers store key-value pairs, but they serve slightly different purposes.
We use std::map when we want keys to remain sorted automatically. We use std::unordered_map when we want fast average-case lookup and do not need sorted order.
In practice, std::unordered_map is often the better choice when we are using a container like a dictionary or lookup table. If ordered traversal matters, std::map is the better fit.
Container adapters
Sometimes, giving full, random access to a container is too much power.
If any part of the program can insert, remove, or read elements arbitrarily, it becomes easy to:
Break invariants
Misuse data structures
Violate the intended processing order
What we want instead is to enforce a specific data-processing rule.
Container adapters deliberately limit how data can be accessed, so it can only be processed in a specific way.
They don’t introduce new data structures; they wrap existing containers and expose only a restricted interface.
std::stack
The std::stack container adapter enforces a last-in, first-out (LIFO) processing rule. This means that the most recently added element is always the first one to be removed. A familiar analogy is a stack of plates: plates can be placed only on top of the stack, and only the top plate can be taken off.
By design, std::stack prevents random access to its elements. You cannot iterate over the stack or access elements in the middle. Instead, the interface allows elements to be added to the top, removed from the top, and inspected at the top. This restriction ensures that data is always processed according to LIFO logic, which is especially useful in scenarios such as undo functionality, function call management, or backtracking algorithms.
Let’s understand this step by step:
Lines 7–9: We populate the stack by pushing data into it.
Line 11: We print out the value at the top of the stack to the console using the
.top()function.Line 13: We use the
.pop()function to remove the top element of the stack.Line 15: The
.top()function now displays a different value at the top of the stack.
std::queue
The std::queue container adapter enforces a first-in, first-out (FIFO) processing rule. In this model, the first element added to the container is the first one removed. A common real-world analogy is a line at a store: customers are served in the same order in which they arrive.
Like std::stack, std::queue restricts access to its elements to prevent misuse. Elements can only be added at the back of the queue and removed from the front. There is no way to access or remove elements from the middle, which guarantees that data is processed fairly and in sequence. This makes std::queue well-suited for task scheduling, request handling, and message processing systems.
Let’s understand this step by step:
Lines 7–9: We populate the queue by pushing data into it. Each new element is added to the back of the queue.
Line 11: We print out the value at the front of the queue to the console using the
.front()function.Line 13: We use the
.pop()function to remove the front element of the queue.Line 15: The
.front()function now displays a different value at the front of the queue.