Standard containers

C++ · Practical · 15 min

Picking the right container is the most common design decision in C++ code and has a big effect on speed and clarity.

The standard library's containers fall into a few families:

  • Sequence: std::vector (the default), std::array (fixed size), std::deque (fast push/pop at both ends), std::list / std::forward_list (linked lists, rarely the right choice).
  • Ordered associative: std::map<K, V> and std::set<K>, kept sorted by key in a balanced tree. Lookup, insert and erase are O(log n), and iteration is in key order.
  • Unordered associative: std::unordered_map and std::unordered_set, hash tables with average O(1) operations and no ordering. Keys need std::hash and ==.
  • Adapters: std::stack, std::queue, std::priority_queue (a heap) on top of another container.

Map APIs have well-known traps. m[key] inserts a default-constructed value if the key is missing, which is convenient for counting (++freq[word]) but surprising for lookups, and unavailable on a const map. To look up without inserting, use find (returns end() when missing), contains (C++20) or at (throws). try_emplace and insert_or_assign are the clear way to insert. Iterate with structured bindings: for (const auto& [key, value] : m).

Rules of thumb: start with vector; use unordered_map for fast key lookup; use map when you need sorted order or range queries like lower_bound. For a small, mostly-read lookup table, a sorted vector often beats both thanks to cache locality; C++23 packages that idea as std::flat_map and std::flat_set.

All containers are RAII values: they free everything they hold, copy deeply and can be moved cheaply.

Example

#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

int main() {
    std::vector<std::string> words{"to", "be", "or", "not", "to", "be"};

    std::unordered_map<std::string, int> freq;       // hash table, average O(1)
    for (const auto& w : words) ++freq[w];           // [] inserts 0 if missing

    std::map<std::string, int> sorted(freq.begin(), freq.end());   // ordered by key
    for (const auto& [word, n] : sorted)             // structured bindings
        std::cout << word << '=' << n << ' ';        // be=2 not=1 or=1 to=2
    std::cout << '\n';

    std::set<std::string> unique(words.begin(), words.end());
    std::cout << unique.size() << ' ' << unique.contains("or") << '\n';   // 4 1

    if (auto it = freq.find("xyz"); it == freq.end())
        std::cout << "no xyz\n";                     // find doesn't insert; [] would
    std::cout << freq.size() << '\n';                // 4
}

Remember

  • vector first; unordered_map for fast lookup; map for sorted keys
  • m[k] inserts when k is missing — use find, contains or at to look up
  • Iterate maps with for (const auto& [k, v] : m)

Watch out for

  • Using if (m[key] == 0) to test for presence, which inserts every key you test.

Check yourself

  1. You need to print all entries sorted by key and occasionally find all keys in a range. Which container fits best?
  2. After `std::map<std::string, int> m; if (m["a"] > 0) {}`, what is m.size()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/container