Standard Library[lib]

C++ map: std::map find, insert, erase and iterate

Section
Standard Library [lib]
Standard
C++17
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
11 min read
In this answer9 clauses
  1. 1Create a std::map and loop over it
  2. 2Look up a key: find, contains, at or operator[]
  3. 3Insert or update: try_emplace, insert_or_assign, emplace, insert or operator[]
  4. 4Erase: by key, in a loop, or by condition
  5. 5The loop that copies every element
  6. 6Look up by std::string_view without building a string
  7. 7Change a key, or move elements to another map
  8. 8std::map, std::unordered_map or std::flat_map
  9. 9Which call for which job

Abstract

A C++ map, std::map, keeps its keys sorted and finds, inserts and erases in logarithmic time. Read with find, contains (C++20) or at, which throws std::out_of_range for a missing key; m[key] inserts a value-initialized element when the key is missing. Add a key only if it is absent with try_emplace, add or overwrite with insert_or_assign (both C++17), and erase by condition with std::erase_if (C++20).

A balanced tree of seven nodes joined by black lines: one root, two children, four leaves. Each node is split into a narrow key part and a wide value part. One leaf is hatched, its link drawn in blue.
Figure 1 — A map keeps each element in its own node, in key order.

A C++ map, std::map<Key, T> from <map>, keeps its elements sorted by key and finds, inserts and erases in logarithmic time. To read a key without changing the map, call find, contains (C++20) or at, which throws std::out_of_range when the key is missing; m[key] inserts a value-initialized T when it is. To add a key only if it is absent, call try_emplace; to add or overwrite, insert_or_assign (both C++17). Loop with for (const auto& [key, value] : m). If you never need the keys in order, consider std::unordered_map.

This answer is written for C++17, which added try_emplace, insert_or_assign, extract, merge and the structured bindings in that loop. C++20 added contains and std::erase_if, C++23 added std::flat_map, and C++26 lets four more members take a std::string_view key when the comparator allows it.

The samples were built with four compilers on three standard libraries: MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64). Each output shown is what all four printed unless the text says otherwise, and every allocation count is from a release build. Since When’s C++17 table gives the first releases with try_emplace and structured bindings.

What changed: a key is added with m.insert(std::make_pair(key, value)) m.try_emplace(key, value) and tested with m.find(key) != m.end() m.contains(key).

since C++17

Create a std::map and loop over it

The running example is a settings map from names to values:

[Example 1:
std::map<std::string, std::string> settings{
    {"retries", "3"}, {"port", "8080"}, {"host", "example.com"}};
settings["timeout"] = "30";             // adds a fourth key

for (const auto& [key, value] : settings)
    std::cout << key << " = " << value << '\n';

— end example]

It printed the keys sorted, not in the order they were written:

[Example 2:
host = example.com
port = 8080
retries = 3
timeout = 30

— end example]

The order is std::less<Key> unless you pass a comparator for std::map as the third template argument. Each element is a std::pair<const Key, T>, the map’s value_type: a loop can change a value, never a key.

Look up a key: find, contains, at or operator[]

Every lookup but m[key] leaves the map as it is:

Call Key present Key missing
m.find(key) an iterator to the element m.end()
m.contains(key), C++20 true false
m.count(key) 1 0
m.at(key) a reference to the value throws std::out_of_range
m[key] a reference to the value inserts T(), returns a reference to it
m.lower_bound(key) an iterator to the element the next greater key, or m.end()
m.upper_bound(key) the next element, or m.end() the next greater key, or m.end()
m.equal_range(key) the element and the next one an empty range at the next greater key
[Example 3:
if (auto it = settings.find("port"); it != settings.end())
    std::cout << "port is " << it->second << '\n';
else
    std::cout << "no port set\n";

std::cout << "size before: " << settings.size() << '\n';
if (settings["user"].empty())       // a read that inserts "user"
    std::cout << "no user set\n";
std::cout << "size after: " << settings.size() << '\n';

— end example]

On a map holding host and port, it printed:

[Example 4:
port is 8080
size before: 2
no user set
size after: 3

— end example]

The read through settings["user"] added "user" with an empty value, because operator[] is specified as return try_emplace(x).first->second; ([map.access]). That insertion is what makes it right for counting: ++counts[word] starts a new word at int(), which is 0.

For the same reason operator[] has no const overload: all four rejected settings["port"] through a const Settings& (both Clangs: no viable overloaded operator[] for type 'const Settings'). Use at there, and wherever a missing key is a bug. Its exception’s what() is each library’s own (Microsoft’s invalid map<K, T> key, libstdc++’s map::at, libc++’s map::at: key not found), so catch the type. contains needs C++20: in C++17 mode (-std=c++17, /std:c++17) all four reject it (GCC: has no member named 'contains').

lower_bound and upper_bound find the nearest key, the lookup an ordered map is for:

[Example 5:
const std::map<int, std::string> ports{
    {80, "http"}, {443, "https"}, {8080, "proxy"}};

auto at_or_after = ports.lower_bound(443);  // first key >= 443
auto after = ports.upper_bound(443);        // first key > 443
auto nearest_up = ports.lower_bound(100);   // 443: no 100, next up
auto none = ports.lower_bound(9000);        // ports.end()

— end example]

It printed:

[Example 6:
443 8080 443 end

— end example]

equal_range(443) returned the same two iterators as the first two calls, on all four.

Insert or update: try_emplace, insert_or_assign, emplace, insert or operator[]

To add a key only if it is absent, call try_emplace; to add it or overwrite its value, call insert_or_assign. Both are C++17 (N4279) and take the key and the value as separate arguments.

Each way to add was run on a map holding port, once with a missing key and once with port. The key k is a const char*, the type a string literal decays to, and the mapped type logs its constructions and assignments.

Call Returns Key missing Key present
m.try_emplace(k, v), C++17 {iterator, bool} inserts; true keeps the old value; false
m.insert_or_assign(k, v), C++17 {iterator, bool} inserts; true assigns v; false
m.emplace(k, v) {iterator, bool} inserts; true keeps the old value; false
m.insert(std::make_pair(k, v)) {iterator, bool} inserts; true keeps the old value; false
m.insert({k, v}) {iterator, bool} inserts; true keeps the old value; false
m[k] = v T& inserts T(), assigns v assigns v

The iterator points to the element with that key, new or old. Behind the same false, the calls did different work on the three libraries, in C++17 mode: emplace and the make_pair insert built a node and freed it, insert({k, v}) built a pair and destroyed it, and try_emplace touched nothing.

emplace is allowed to: the element “may be constructed even if there already is an element with the key in the container, in which case the newly constructed element will be destroyed immediately” (cppreference on std::map::emplace). With a move-only value, that decides whether your object survives:

[Example 7:
auto conn = std::make_unique<Connection>("db-2");
pool.emplace("db", std::move(conn));        // "db" is taken
// ...
conn = std::make_unique<Connection>("db-3");
pool.emplace(key, std::move(conn));         // key: a std::string
// ...
conn = std::make_unique<Connection>("db-4");
pool.try_emplace("db", std::move(conn));    // "db" is taken

— end example]

Call, with "db" taken Element built conn afterwards
emplace("db", std::move(conn)) yes, then destroyed, on the three libraries in C++17 mode null
emplace(key, std::move(conn)), key a std::string no, on the three libraries in C++17 mode; not guaranteed still yours, here
try_emplace, with either key no still yours
insert({"db", std::move(conn)}) no; a pair took it and was destroyed null
insert_or_assign("db", std::move(conn)) no; assigned null: it replaced the old one

Building nothing for a std::string key is each library’s optimization, not a guarantee, and it held here for a two-argument emplace on a map with the default allocator. On GCC 16.2’s libstdc++, the same call built the element and took conn under -std=c++14, or on a map with another allocator; Microsoft’s STL and libc++ kept it in both cases.

std::move(conn) moved nothing by itself (what std::move really does): the call decides. The one call guaranteed to leave its arguments alone when the key exists is try_emplace, which then has “no effect” ([map.modifiers]).

m[k] = v also needs a default constructor for T; the other calls do not. With a Port type that has none, try_emplace, insert_or_assign and emplace built on all four, and ports["http"] = Port(80) failed on all four inside the library’s headers rather than on that line (MSVC: error C2512: 'Port::Port': no appropriate default constructor available).

Erase: by key, in a loop, or by condition

Call Erases Returns
m.erase(key) the element with that key, if any how many it erased: 1 or 0
m.erase(it) the element at it the iterator after it
m.erase(first, last) the elements from first up to last last
std::erase_if(m, pred), C++20 each element pred accepts how many it erased
[Example 8:
std::map<std::string, int> counts{{"host", 1}, {"port", 4},
                                  {"retries", 0}, {"timeout", 2},
                                  {"user", 0}};
int& port = counts.at("port");      // a reference to one element

std::cout << counts.erase("retries") << '\n';   // 1: erased
std::cout << counts.erase("retries") << '\n';   // 0: was not there

for (auto it = counts.begin(); it != counts.end();) {
    if (it->second == 0)
        it = counts.erase(it);      // returns the next element
    else
        ++it;
}
// ...
std::cout << "port is still " << port << '\n';

— end example]

It printed the two results, the elements left, and port:

[Example 9:
1
0
host 1
port 4
timeout 2
port is still 4

— end example]

it = counts.erase(it) is the loop idiom because erasing invalidates only iterators and references to the erased elements ([associative.reqmts]): the old it is gone, the returned one is valid, and port still read 4. In C++20, std::erase_if writes that loop for you:

[Example 10:
auto erased = std::erase_if(counts, [](const auto& item) {
    return item.second == 0;
});                                 // C++20

— end example]

On the same map it returned 2 and left the same three elements. In C++17 mode all four reject it (Clang: no member named 'erase_if' in namespace 'std').

The loop that copies every element

for (const std::pair<std::string, int>& item : counts) compiles, and copies every element. The element type is std::pair<const std::string, int>, so the reference binds to a temporary converted from it. A key type that counts its copies showed it, over three elements:

[Example 11:
for (const std::pair<Word, int>& item : counts)    // not the type
    total += item.second;
// ...
for (const auto& [word, n] : counts)
    total += n;
// ...
for (auto [word, n] : counts)       // a copy of each element
    total += n;

— end example]

The five loops (two left out above) printed:

[Example 12:
const std::pair<Word, int>&: 3 copies
const auto&: 0 copies
const auto& [word, n]: 0 copies
const std::pair<const Word, int>&: 0 copies
auto [word, n]: 3 copies

— end example]

Write const auto& or const auto& [key, value]; without the &, structured bindings copy each element too. GCC and both Clangs warn about the first loop at -Wall -Wextra (-Wrange-loop-construct: the loop variable “binds to a temporary”); MSVC at /W4 said nothing.

The clang-tidy 19.1.5 bundled with Visual Studio reports it with performance-implicit-conversion-in-loop: “the type of the loop variable ‘item’ is different from the one returned by the iterator and generates an implicit conversion”. In the same file, readability-container-contains reported counts.count("port") > 0 (“use ‘contains’ to check for membership”) but not counts.find("host") != counts.end(), which clang-tidy trunk reports against libstdc++ and libc++. clang-tidy and the other analyzers are on Toolchain.

Look up by std::string_view without building a string

With the default comparator, std::less<std::string>, find takes only a const std::string&. A std::string_view does not compile there (Clang: no matching member function for call to 'find'), and find(std::string(sv)) copies the key into a new string. Declare the map with std::less<>, a transparent comparator that compares a std::string with a view directly:

[Example 13:
std::map<std::string, std::string, std::less<>> settings{
    {std::string(key), "300"}};
// ...
auto a = plain.find(std::string(key));  // builds a std::string
// ...
auto b = settings.find(key);            // compares in place
auto n = settings.count(key);
auto c = settings.lower_bound(key);

— end example]

key is a 40-character std::string_view cut from a config line, and plain the same map with the default comparator. In release builds, plain.find(std::string(key)) made 1 allocation and the three calls on settings made none, on all three libraries. The members that take the view, with std::less<>:

  • C++14 (N3657): find, count, lower_bound, upper_bound, equal_range.
  • C++20: contains.
  • C++23 (P2077R3): erase, extract; libc++ 23.1 lacks both, even in C++26 mode.
  • C++26 (P2363R5): operator[], at, try_emplace, insert_or_assign; of the libraries tested, only GCC 16.2’s libstdc++ accepts them, and Microsoft’s STL and libc++ 23.1 reject settings[key] in C++26 mode.

std::unordered_map takes a view from C++20, given a transparent equality such as std::equal_to<> and a transparent hash. The standard library has none for std::string, so you write it:

[Example 14:
struct StringHash {             // hashes std::string and string_view
    using is_transparent = void;
    std::size_t operator()(std::string_view s) const noexcept {
        return std::hash<std::string_view>{}(s);
    }
};

using Settings = std::unordered_map<std::string, std::string,
                                    StringHash, std::equal_to<>>;

— end example]

With it, find(key) made no allocation on all three libraries; in C++17 mode it does not compile.

Change a key, or move elements to another map

A key is const inside the map, so to change one, take its node out. extract (C++17) returns a node handle whose key() can be assigned, and insert puts it back:

[Example 15:
auto node = settings.extract("hostname");   // the node, unlinked
node.key() = "host";        // a node's key can be changed
settings.insert(std::move(node));
// ...
settings.merge(defaults);   // "port" stays in defaults

— end example]

The rename and the merge made 0 allocations on all three libraries; cppreference calls extract “the only way to change a key of a map element without reallocation”. merge moved retries and timeout out of defaults and left port=80 there, because settings already had a port. extract of a key the map lacks returns an empty node handle.

std::map, std::unordered_map or std::flat_map

Container Order Find Insert The key needs
std::map by key logarithmic logarithmic; no iterator invalidated a comparator, < by default
std::unordered_map, C++11 none; it differed on each library average constant, worst linear average constant; a rehash invalidates iterators, not references a hash and ==
std::flat_map, C++23 by key logarithmic linear; later elements shift, and any iterator or reference may be invalidated a comparator, < by default

A std::flat_map (header <flat_map>) keeps the keys in one sorted container and the values in another, std::vector by default, with no nodes; [flat.map.overview] exempts it from the associative containers’ invalidation guarantees.

Two rows with the keys host, port, retries, timeout and user. Top, std::map: five separate boxes joined by thin lines as a tree, retries at the root. Bottom, std::flat_map: the same keys in one row of adjoining cells.
Figure 2 — A std::map allocates a node for every element and never moves it; a std::flat_map keeps the keys side by side and shifts them on insert.

A program inserted the same 1,000 int keys, in a scrambled order, into each container, and a replaced operator new counted the allocations. For std::flat_map:

[Example 16:
std::flat_map<int, int> m;                  // C++23
for (int i = 0; i < 1000; ++i) m.emplace(key_at(i), i);

— end example]

Allocations (bytes) Microsoft’s STL libstdc++ libc++
an empty std::map 1 0 0
an empty std::unordered_map 2 0 0
std::map, 1,000 keys 1,000 (40,000) 1,000 (40,000) 1,000 (40,000)
std::unordered_map, 1,000 keys 1,003 (49,678) 1,007 (33,080) 1,010 (49,384)
std::unordered_map, 1,000 keys after reserve(1000) (not counting the reserve itself) 1,000 (24,000) 1,000 (16,000) 1,000 (24,000)
std::flat_map, 1,000 keys no <flat_map> 22 (16,376) 22 (16,376)

A std::map allocated once per element on all three libraries; on libstdc++ and libc++, a std::flat_map allocated 22 times for the same 1,000 keys. Microsoft’s STL in MSVC 19.44 has no <flat_map> (fatal error C1083: Cannot open include file: 'flat_map': No such file or directory); Since When’s C++23 table lists it from libstdc++ 15.1, libc++ 20 and Visual Studio 2026 18.6.

Which call for which job

Job Call Since
Test for a key m.contains(key) C++20
Test for a key before C++20 m.find(key) != m.end() before C++17
Read a value that must be there m.at(key) before C++17
Read a value or a default (no member does it) auto it = m.find(key); it != m.end() ? it->second : fallback before C++17
Nearest key at or above m.lower_bound(key) before C++17
Add if absent m.try_emplace(key, value) C++17
Add or overwrite m.insert_or_assign(key, value) C++17
Count occurrences ++m[key] before C++17
Erase by condition std::erase_if(m, pred) C++20
Loop for (const auto& [key, value] : m) C++17
Look up by std::string_view a map declared with std::less<> C++14
Rename a key extract, key(), insert C++17

More answers about the library’s containers and types are in Standard library.

Questions

Is std::map sorted?

Yes, by key. A std::map orders its keys with std::less of the key type unless you give it another comparator as its third template argument, and a loop from begin() to end() visits them in that order. A map given the keys retries, port, host and timeout, in that order, printed them as host, port, retries, timeout on all four compilers tested.

Is std::map a red-black tree?

The standard does not say. It specifies std::map by its behavior, unique keys with logarithmic search, insertion and erasure, and names no data structure; cppreference notes that it is usually implemented as a red-black tree. GCC’s libstdc++ calls its node type _Rb_tree_node, a name that shows up in its error messages.

How do I sort a std::map by value?

A std::map stays sorted by key, so copy it out. Construct a std::vector of pairs whose key is not const (for a map of std::string to int, pairs of std::string and int) from m.begin() and m.end(), and std::sort it with a comparator on .second. A vector whose type is deduced from the map’s iterators holds pairs with a const key, which cannot be assigned, so std::sort does not compile. Counts of port 3, host 2 and retries 1 sorted as port, host, retries on all four compilers tested.

How do I get the smallest or largest key in a std::map?

Read the first or the last element: m.begin()->first is the smallest key, and m.rbegin()->first or std::prev(m.end())->first is the largest, because a std::map keeps its elements in key order. Both need a non-empty map, so test m.empty() first.

Read next

  1. C++ int to string: to_string, format or to_chars, not itoaStandard Library · Miriam Castellanos · written for C++17
  2. std::optional in C++: how to use it and when not toStandard Library · Miriam Castellanos · written for C++17
  3. std::expected in C++23: return a value or an errorStandard Library · Miriam Castellanos · written for C++23