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
- 1Create a std::map and loop over it
- 2Look up a key: find, contains, at or operator[]
- 3Insert or update: try_emplace, insert_or_assign, emplace, insert or operator[]
- 4Erase: by key, in a loop, or by condition
- 5The loop that copies every element
- 6Look up by std::string_view without building a string
- 7Change a key, or move elements to another map
- 8std::map, std::unordered_map or std::flat_map
- 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 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::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:
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:
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::, 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 |
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:
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). ([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::), 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:
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:
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:: |
{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:
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:: |
yes, then destroyed, on the three libraries in C++17 mode | null |
emplace(key, std::, 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:: |
no; a pair took it and was destroyed | null |
insert_or_assign("db", std:: |
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::).
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::, C++20 |
each element pred accepts |
how many it erased |
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:
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:
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:: compiles, and copies every element. The element type is std::, so the reference binds to a temporary converted from it. A key type that counts its copies showed it, over three elements:
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:
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::, 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:: 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:
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:: 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 rejectsettings[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:
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:
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.

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:
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:: |
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.
Sources
- std::mapcppreference.com
- std::map::try_emplacecppreference.com
- std::map::emplacecppreference.com
- [map.access] Element accessC++ standard working draft
- [associative.reqmts] Associative containers, requirementsC++ standard working draft
- N4279: Improved insertion interface for unique-key mapsWG21
- P2363R5: Extending associative containers with the remaining heterogeneous overloadsWG21
- performance-implicit-conversion-in-loopclang-tidy documentation