Turning Learners Into Developers
Codekilla
CODEKILLA
C++ 8 min

C++ STL Containers: Which One Should You Actually Use?

The C++ Standard Template Library gives you a toolkit of pre-built data structures called **containers**.

Rahul Chaudhary Sat Oct 17 2026
What is the C++ Standard Template Library (STL)?

The C++ Standard Template Library gives you a toolkit of pre-built data structures called containers. Instead of writing your own linked list or hash table from scratch, you grab std::vector, std::map, or one of a dozen other containers that handle memory, iteration, and common operations for you. Each container has trade-offs in speed, memory layout, and what operations it does best. Your job is to pick the right tool for the job—because choosing badly can tank performance or bloat your code with unnecessary complexity.

Think of STL containers as specialized boxes for storing data. Some boxes let you access items by index in constant time. Others keep everything sorted automatically. Some optimize for fast insertion at the front, while others shine when you're mostly adding to the back. Understanding these differences transforms you from someone who just uses std::vector for everything into a developer who writes genuinely efficient C++.

Why It Matters
  • Performance scales: A std::vector lookup is O(1), but searching an unsorted std::list is O(n). In a game loop running 60 times per second, that difference is the gap between smooth gameplay and frame drops.
  • Memory locality wins: Modern CPUs prefetch contiguous memory. Containers like std::vector exploit cache lines; node-based containers like std::list scatter allocations and kill cache performance.
  • API clarity: Using std::set signals "unique sorted elements" to anyone reading your code. Using a raw array with manual sorting doesn't.
  • Iterator invalidation traps: Insert into a std::vector and all your iterators might become invalid. Understanding container guarantees prevents subtle bugs.
  • Zero-cost abstractions: STL containers compile down to tight machine code. You're not sacrificing speed for convenience—you're gaining type safety and correctness checks.
Sequence Containers: Linear Storage

Sequence containers store elements in a specific order you control. You decide where things go, and the container maintains that sequence.

std::vector is your default choice. It's a dynamically-resizing array with contiguous memory. You get constant-time random access via operator[], and appending to the back is amortized O(1). The catch? Inserting in the middle is O(n) because you have to shift everything. Resizing triggers reallocation, which invalidates all iterators and pointers.

cpp
#include <vector>
#include <iostream>

int main() {
    std::vector<int> scores = {10, 20, 30};
    scores.push_back(40);          // Fast: O(1) amortized
    scores.insert(scores.begin(), 5); // Slow: O(n), shifts everything
    
    for (size_t i = 0; i < scores.size(); ++i) {
        std::cout << scores[i] << " "; // Random access: O(1)
    }
    // Output: 5 10 20 30 40
}

std::deque (double-ended queue) lets you insert/remove efficiently at both front and back. Unlike std::vector, it doesn't store elements in one contiguous block—it uses chunks of memory. This means iterators stay valid when you grow the deque, but you lose some cache locality.

std::list is a doubly-linked list. Insertions and deletions anywhere are O(1) if you already have an iterator to that position. But there's no random access—reaching the middle is O(n). The killer downside? Every element requires separate heap allocation. Cache performance is terrible. Use it only when you need stable iterators (ones that never invalidate) or frequent mid-sequence splicing.

ContainerRandom AccessInsert FrontInsert BackInsert MiddleMemory Layout
std::vectorO(1)O(n)O(1)*O(n)Contiguous
std::dequeO(1)O(1)O(1)O(n)Chunked
std::listO(n)O(1)O(1)O(1)**Scattered nodes

*Amortized when capacity allows
**With iterator to position

Associative Containers: Sorted Key-Value Storage

Associative containers organize elements by keys and keep them sorted. They're built on balanced binary search trees (usually red-black trees), so lookups, insertions, and deletions are all O(log n).

std::set stores unique keys with no duplicates. It's perfect when you need to track membership and occasionally iterate in sorted order. std::multiset allows duplicates.

std::map pairs keys with values. Think of it as a dictionary: look up an employee ID (key) and get their salary (value). std::multimap allows duplicate keys.

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

int main() {
    std::map<std::string, int> inventory;
    inventory["sword"] = 5;
    inventory["shield"] = 2;
    inventory["potion"] = 12;
    
    // Lookup: O(log n)
    if (inventory.find("sword") != inventory.end()) {
        std::cout << "Swords in stock: " << inventory["sword"] << "\n";
    }
    
    // Iterates in sorted key order (alphabetical for strings)
    for (const auto& [item, count] : inventory) {
        std::cout << item << ": " << count << "\n";
    }
    // Output: potion: 12, shield: 2, sword: 5
}

The sorted property comes with overhead. If you don't need order and just want fast lookups, reach for unordered containers instead.

Unordered Containers: Hash Table Speed

Unordered containers use hash tables for average-case O(1) lookups, insertions, and deletions. The trade-off? Elements aren't sorted, and worst-case performance degrades to O(n) if your hash function produces collisions.

std::unordered_set and std::unordered_map are the workhorses here. Use them when you need fast membership tests or key-value lookups and don't care about order.

cpp
#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_set<int> seen = {3, 7, 11};
    
    seen.insert(15);            // O(1) average
    
    if (seen.count(7)) {        // O(1) average lookup
        std::cout << "7 was seen\n";
    }
    
    // Iteration order is undefined
    for (int val : seen) {
        std::cout << val << " ";
    }
    // Possible output: 15 11 3 7 (order unpredictable)
}

Hash containers require your key type to be hashable. Standard types (integers, strings) work out of the box. For custom types, you'll need to provide a hash function and equality operator.

ContainerLookupInsert/DeleteSorted?Use When
std::set / std::mapO(log n)O(log n)YesNeed sorted iteration
std::unordered_set / std::unordered_mapO(1) avgO(1) avgNoNeed raw speed
Container Adaptors: Restricted Interfaces

Container adaptors wrap sequence containers and restrict the interface to enforce specific behavior. They don't provide iterators—you interact through specialized methods.

std::stack is LIFO (last in, first out). You push() onto the top and pop() from the top. It's typically backed by std::deque but you can specify std::vector or std::list.

std::queue is FIFO (first in, first out). You push() at the back and pop() from the front. Backed by std::deque by default.

std::priority_queue is a heap. Elements are ordered by priority, and you always pop the highest-priority element. Backed by std::vector with heap operations.

cpp
#include <stack>
#include <queue>
#include <iostream>

int main() {
    std::stack<int> undo_stack;
    undo_stack.push(1);
    undo_stack.push(2);
    std::cout << "Top: " << undo_stack.top() << "\n"; // 2
    undo_stack.pop();
    std::cout << "Top: " << undo_stack.top() << "\n"; // 1
    
    std::priority_queue<int> pq;
    pq.push(10);
    pq.push(30);
    pq.push(20);
    std::cout << "Highest: " << pq.top() << "\n"; // 30
}

Use adaptors when the semantics match your problem domain exactly. They signal intent and prevent misuse.

Special-Purpose Containers

std::array is a fixed-size array wrapper with STL-style iterators. Size is a template parameter, so it's known at compile time. It lives on the stack (if declared locally) and has zero overhead compared to a raw array.

cpp
#include <array>
#include <algorithm>
#include <iostream>

int main() {
    std::array<int, 4> coords = {10, 20, 30, 40};
    std::sort(coords.begin(), coords.end()); // Works with STL algorithms
    
    for (int c : coords) {
        std::cout << c << " ";
    }
}

std::forward_list is a singly-linked list. It uses less memory per node than std::list (no backward pointer) but you can only traverse forward. Rarely used—only when memory per element matters critically.

std::string is technically a sequence container specialized for characters. It has small-string optimization (SSO): short strings live in the object itself without heap allocation.

Quick Cheat Sheet
NeedReach for
Default go-to containerstd::vector
Fast lookup by key, no order neededstd::unordered_map / std::unordered_set
Fast lookup by key, need sorted iterationstd::map / std::set
Insert/remove at both endsstd::deque
LIFO operations onlystd::stack
FIFO operations onlystd::queue
Always pop highest prioritystd::priority_queue
Fixed size known at compile timestd::array
Frequent mid-sequence splicing, stable iteratorsstd::list
Common Mistakes
  • Using std::list by default: You think "linked list = fast inserts" but forget that finding where to insert is O(n). Cache misses murder real-world performance. Benchmark before choosing std::list.
  • Ignoring reserve() for vectors: If you know roughly how many elements you'll store, call reserve() upfront. Otherwise, std::vector reallocates multiple times as it grows, copying everything each time.
  • Assuming std::map lookup is O(1): It's O(log n). If you don't need sorted order, std::unordered_map is almost always faster.
  • Not checking find() before dereferencing: For associative containers, find() returns an iterator. If the key doesn't exist, you get end(). Dereferencing end() is undefined behavior—always check.
  • Forgetting iterator invalidation rules: Inserting into a std::vector can reallocate, invalidating all iterators. Erasing from a std::map only invalidates the erased iterator. Know the rules or face crashes.
  • Using raw operator[] on maps for existence checks: map[key] inserts a default-constructed value if the key doesn't exist. Use find() or count() to check membership without side effects.

💡 Think Like a Programmer: The "best" container depends entirely on access patterns. Profile your code's hot paths—if you're iterating sequentially most of the time, std::vector wins. If lookups dominate, hash tables reign. Let data guide your choice, not folklore.

// was this useful?
Did this article answer your question?
// C++ · published by Codekilla
// related articles

Keep Reading