The STL map in C++: A Practical Tutorial

The C++ Standard Template Library (STL) provides a container called std::map that stores data as key–value pairs. Think of it as a dictionary: you look up a word (the key) and get its definition (the value). The map keeps all its keys sorted automatically, and every key must be unique.

To use a map, include its header:

#include <map>
#include <iostream>
#include <string>
using namespace std;

Example 1 — Creating a Map and Inserting Entries

A map is declared as map<KeyType, ValueType>. There are three common ways to insert entries.

#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    map<string, int> scores;   // keys are strings, values are ints

    // Method 1: bracket operator (creates the key if it doesn't exist)
    scores["Alice"] = 95;
    scores["Bob"]   = 87;

    // Method 2: insert() with make_pair
    scores.insert(make_pair("Carol", 91));

    // Method 3: insert() with brace initializer (C++11 and later)
    scores.insert({"Dave", 78});

    // Print all entries — notice they are sorted by key (alphabetical)
    for (auto& entry : scores) {
        cout << entry.first << ": " << entry.second << "\n";
    }
    return 0;
}

Output:

Alice: 95
Bob: 87
Carol: 91
Dave: 78

Key point: entry.first is the key and entry.second is the value. The map automatically keeps entries sorted by key — you never have to sort manually.


Example 2 — Accessing Values

There are two safe ways to read a value and one way that throws an exception on a missing key.

#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    map<string, int> scores = {
        {"Alice", 95}, {"Bob", 87}, {"Carol", 91}
    };

    // --- Method 1: bracket operator ---
    // WARNING: if the key does not exist, it is created with value 0!
    cout << "Alice's score: " << scores["Alice"] << "\n";

    // --- Method 2: at() ---
    // Safe read: throws std::out_of_range if the key is missing
    cout << "Bob's score:   " << scores.at("Bob") << "\n";

    // --- Method 3: find() ---
    // Returns an iterator to the entry, or map::end() if not found
    auto it = scores.find("Carol");
    if (it != scores.end()) {
        cout << "Carol's score: " << it->second << "\n";
    }

    // Demonstrating the [] pitfall
    cout << "Erin's score:  " << scores["Erin"] << "\n";  // inserts Erin = 0!
    cout << "Map now has " << scores.size() << " entries\n";

    return 0;
}

Output:

Alice's score: 95
Bob's score:   87
Carol's score: 91
Erin's score:  0
Map now has 4 entries

Key point: Using [] on a key that does not exist inserts that key with a default value. Use find() or at() when you only want to read.


Example 3 — Checking Whether a Key Exists

Before reading or modifying a value, it is good practice to confirm the key is present.

#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    map<string, string> capitals = {
        {"France", "Paris"},
        {"Japan",  "Tokyo"},
        {"Brazil", "Brasília"}
    };

    // --- Method 1: count() ---
    // Returns 1 if the key exists, 0 if not (maps have unique keys)
    string country = "Japan";
    if (capitals.count(country)) {
        cout << country << " -> " << capitals[country] << "\n";
    }

    // --- Method 2: find() ---
    // Preferred when you also need the value — avoids a second lookup
    auto it = capitals.find("Germany");
    if (it == capitals.end()) {
        cout << "Germany not found in the map\n";
    } else {
        cout << "Germany -> " << it->second << "\n";
    }

    // C++20: contains() — the clearest option
    // if (capitals.contains("France")) { ... }

    return 0;
}

Output:

Japan -> Tokyo
Germany not found in the map

Example 4 — Iterating Over a Map

There are three common iteration styles. All of them visit entries in sorted key order.

#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    map<string, int> inventory = {
        {"pencil", 120}, {"notebook", 45}, {"eraser", 200}, {"ruler", 30}
    };

    // --- Style 1: range-based for with auto& (C++11, recommended) ---
    cout << "=== Inventory ===\n";
    for (auto& [item, qty] : inventory) {   // structured binding (C++17)
        cout << item << ": " << qty << "\n";
    }

    // --- Style 2: range-based for without structured binding ---
    for (auto& entry : inventory) {
        // entry.first = key, entry.second = value
        if (entry.second < 50) {
            cout << "Low stock: " << entry.first << "\n";
        }
    }

    // --- Style 3: explicit iterator ---
    cout << "=== Iterator walk ===\n";
    for (map<string,int>::iterator it = inventory.begin();
         it != inventory.end(); ++it) {
        cout << it->first << " -> " << it->second << "\n";
    }

    return 0;
}

Output:

=== Inventory ===
eraser: 200
notebook: 45
pencil: 120
ruler: 30
Low stock: notebook
Low stock: ruler
=== Iterator walk ===
eraser -> 200
notebook -> 45
pencil -> 120
ruler -> 30

Example 5 — Updating and Erasing Entries

#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    map<string, int> ages = {
        {"Alice", 30}, {"Bob", 25}, {"Carol", 28}, {"Dave", 35}
    };

    // --- Update: bracket operator assigns a new value ---
    ages["Bob"] = 26;   // Bob had a birthday
    cout << "Bob's updated age: " << ages["Bob"] << "\n";

    // --- Erase by key ---
    ages.erase("Dave");
    cout << "After erasing Dave, size = " << ages.size() << "\n";

    // --- Erase by iterator (useful inside a loop) ---
    auto it = ages.find("Carol");
    if (it != ages.end()) {
        ages.erase(it);
    }
    cout << "After erasing Carol, size = " << ages.size() << "\n";

    // --- Erase entries matching a condition (C++20 erase_if) ---
    // erase_if(ages, [](auto& e) { return e.second < 28; });

    // Print what remains
    for (auto& [name, age] : ages) {
        cout << name << ": " << age << "\n";
    }

    return 0;
}

Output:

Bob's updated age: 26
After erasing Dave, size = 3
After erasing Carol, size = 2
Alice: 30
Bob: 26

Example 6 — Useful Map Methods at a Glance

#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    map<int, string> m = {{1, "one"}, {2, "two"}, {3, "three"}};

    cout << "size()  = " << m.size()  << "\n";   // number of entries
    cout << "empty() = " << m.empty() << "\n";   // 0 (false) — not empty

    // Smallest and largest key
    cout << "First key: " << m.begin()->first  << "\n";  // 1
    cout << "Last key:  " << prev(m.end())->first << "\n"; // 3

    // lower_bound / upper_bound — find a range of keys
    // lower_bound(k): first key >= k
    // upper_bound(k): first key >  k
    auto lo = m.lower_bound(2);   // points to key 2
    auto hi = m.upper_bound(2);   // points to key 3
    cout << "lower_bound(2): " << lo->first << "\n";
    cout << "upper_bound(2): " << hi->first << "\n";

    m.clear();
    cout << "After clear(), size = " << m.size() << "\n";

    return 0;
}

Output:

size()  = 3
empty() = 0
First key: 1
Last key:  3
lower_bound(2): 2
upper_bound(2): 3
After clear(), size = 0

Example 7 — Real-World Use Case: Word Frequency Counter

One of the most common uses of map is counting occurrences. The [] operator’s default-to-zero behaviour makes this elegant.

#include <map>
#include <iostream>
#include <sstream>
#include <string>
#include <algorithm>
using namespace std;

int main() {
    string text = "the quick brown fox jumps over the lazy dog "
                  "the fox and the dog";

    map<string, int> freq;

    // Tokenize and count
    istringstream ss(text);
    string word;
    while (ss >> word) {
        freq[word]++;   // creates entry with 0 if new, then increments
    }

    // Print in alphabetical order (map is already sorted)
    cout << "=== Word Frequencies ===\n";
    for (auto& [w, count] : freq) {
        cout << w << ": " << count << "\n";
    }

    // Find the most common word
    auto max_it = max_element(freq.begin(), freq.end(),
        [](auto& a, auto& b) { return a.second < b.second; });
    cout << "\nMost common: \"" << max_it->first
         << "\" (" << max_it->second << " times)\n";

    return 0;
}

Output:

=== Word Frequencies ===
and: 1
brown: 1
dog: 2
fox: 2
jumps: 1
lazy: 1
over: 1
quick: 1
the: 4

Most common: "the" (4 times)

Example 8 — Map with a Custom Key or Value Type

Any type can be a key as long as the < operator is defined. Structs need a custom comparator.

#include <map>
#include <iostream>
#include <string>
using namespace std;

struct Point {
    int x, y;
    // Maps need operator< to sort keys
    bool operator<(const Point& other) const {
        if (x != other.x) return x < other.x;
        return y < other.y;
    }
};

int main() {
    map<Point, string> grid;

    grid[{0, 0}] = "origin";
    grid[{1, 0}] = "right";
    grid[{0, 1}] = "up";
    grid[{1, 1}] = "diagonal";

    for (auto& [pt, label] : grid) {
        cout << "(" << pt.x << "," << pt.y << ") = " << label << "\n";
    }

    return 0;
}

Output:

(0,0) = origin
(0,1) = up
(1,0) = right
(1,1) = diagonal

map vs unordered_map — Which Should You Use?

Featuremapunordered_map
Header<map><unordered_map>
Key orderSorted (ascending)No guaranteed order
Lookup / insert / eraseO(log n)O(1) average, O(n) worst
Key requirementMust support <Must support == and hash
MemoryLower overheadHigher (hash table buckets)
Best forSorted output, range queriesMaximum lookup speed

Use map when you need entries in sorted order or need lower_bound/upper_bound. Use unordered_map when raw lookup speed matters and order is unimportant.


Quick Reference: Common map Operations

OperationCodeNotes
Declaremap<K,V> m;
Insert / updatem[key] = val;Creates if missing
Safe insert (no overwrite)m.insert({key, val});Ignores if key exists
Read (safe)m.at(key)Throws if missing
Check existencem.count(key)Returns 0 or 1
Find iteratorm.find(key)Returns end() if missing
Erase by keym.erase(key)
Erase by iteratorm.erase(it)
Sizem.size()
Empty?m.empty()
Clear allm.clear()
First/last keym.begin()->first / prev(m.end())->first
Scroll to Top