Using std::map Wisely With Modern C++
In this article, see some reasons to use std::map with Modern C++.
Join the DZone community and get the full member experience.
Join For Freestd::map and its siblings (std::multimap, std::unordered_map/multimap) used to be my favourite containers when I was doing competitive programming. In fact, I still like them(though using less frequently nowadays). And with Modern C++, we now have more reasons to use std::map. That's why I have decided to address this topic by writing an article summarizing these new features. So, without much gibberish, let's dive-in directly.
std::map::contains (C++20)
std::map::contains
member function is a good step towards code expressiveness. And I am also tire of writing:
if (auto search = freq_of.find(2); search != freq_of.end()) {
cout << "Found" << endl;
}
// Where assume, freq_of = map<uint32_t, uint32_t>{{3, 1}, {1, 1}, {2, 1}};
xxxxxxxxxx
if (freq_of.contains(2)) {
cout << "Found" << endl;
}
The code we write is written first for human consumption & only secondarily for the computer to understand.
- John Sonmez
std::map::try_emplace (C++17)
While inserting into the map, we have 2 different possibilities:- The key doesn't exist yet. Create a fresh key-value pair.
- The key does exist already. Take the existing item and modify it.
std::map
is by using
operator[ ]
,
std::map::insert
or
std::map::emplace
. But, in all of these cases, we have to bear the cost of default/specialized constructor or assignment call. And the worst part is if an item already exists, we have to drop the freshly created item.
xxxxxxxxxx
int main() {
vector v{3, 4, 5, 8, 7, 3, 5, 2, 4};
map<uint32_t, uint32_t> freq_of;
for (const auto &n : v) {
if (const auto &[it, inserted] = freq_of.emplace(n, 1); !inserted) {
it->second++; // Exists already
}
}
assert(freq_of[3] == 2);
return EXIT_SUCCESS;
}
xxxxxxxxxx
if (const auto &[it, inserted] = freq_of.try_emplace(n, 1); !inserted) {
it->second++;
}
std::map::try_emplace
.
std::map::insert_or_assign (C++17)
When you have to insert element anyhow. For the sake of convenience, you use std::map::operator[ ]. Which is OK( and dangerous)! Unless you have any constraint on insertion or assignment.std::map::operator[ ]
isn't feasible. Rather,
std::map::insert_or_assign
is more appropriate and returns more information than std::map::operator[ ]
. It also does not require default-constructibility of the mapped type. Consider the following example for the same.
xxxxxxxxxx
int main() {
vector v{8, 3, 9, 5, 8};
map<uint32_t, uint32_t> freq_of;
for (auto &&n : v) {
const auto &[it, is_inserted] = freq_of.insert_or_assign(n, 1);
if (!is_inserted) { // remove all lesser element then current one if repeated
freq_of.erase(begin(freq_of), it);
}
}
assert((freq_of == decltype(freq_of){
{8, 1},
{9, 1},
}));
return EXIT_SUCCESS;
}
std::map::insert With Hint (C++11/17)
Looking up items in anstd::map
takes
O(log(n))
time. This is the same for inserting new items. Because the position where to insert them must looked up. Naive insertion of
M
new items would thus take
O(M * log(n))
time.
std::map
insertion functions accept an optional insertion hint parameter. The insertion hint is basically an iterator, which points near the future position of the item that is to be inserted. If the hint is correct, then we get amortized
O(1)
insertion time.
xxxxxxxxxx
int main() {
map<uint32_t, string> m{{2, ""}, {3, ""}};
auto where(end(m));
for (const auto &n : {8, 7, 6, 5, 4, 3, 2, 1}) { // Items in non-incremental order
where = m.insert(where, {n, ""});
}
// How it is not done!
// m.insert(end(m), {0, ""});
for (const auto &[key, value] : m) {
cout << key << " : " << value << endl;
}
return EXIT_SUCCESS;
}
O(log(n))
performance again.
**Note:* It is important to know that before C++11, insertion hints were considered correct when they pointed before the position of the newly inserted item.*
std::map::merge (C++17)
Same as std::list:splice, which transfers the elements from one list to another. we havestd::map::merge
which can merge the two same type of
std::map
.
xxxxxxxxxx
int main() {
map<uint32_t, string> fruits{{5, "grapes"}, {2, "tomoto"}};
map<uint32_t, string> person{{2, "mickel"}, {10, "shree"}};
map<uint32_t, string> fruits_and_persons;
fruits_and_persons.merge(fruits);
assert(fruits.size() == 0);
fruits_and_persons.merge(person);
assert(person.size() == 1);
assert(person.at(2) == "mickel"); // Won't overwrite value at 2 i.e.`mickel`
assert((fruits_and_persons == decltype(fruits){
{2, "tomoto"},
{5, "grapes"},
{10, "shree"},
}));
return EXIT_SUCCESS;
}
std::map::extract (C++17)
Unlikestd::map::merge
that transfers the elements in bulk,
std::map::extract
along with std::map::insert
transfers element piecewise. But what is the more compelling application of
std::map::extract
is modifying keys.
std::map
keys are always unique and sorted. Hence, It is crucial that users cannot modify the keys of map nodes that are already inserted. In order to prevent the user from modifying the key items of perfectly sorted map nodes, the
const qualifier is added to the key type.
std::map
the wrong way. But what if we really need to change the keys of some map items?
xxxxxxxxxx
int main() {
map<int, string> race_scoreboard{{1, "Mickel"}, {2, "Shree"}, {3, "Jenti"}};
using Pair = map<int, string>::value_type;
{
auto Jenti(race_scoreboard.extract(3));
auto Mickel(race_scoreboard.extract(1));
swap(Jenti.key(), Mickel.key());
auto [it, is_inserted, nh] = race_scoreboard.insert(move(Jenti)); // nh = node handle
assert(*it == Pair(1, "Jenti") && is_inserted == true && nh.empty());
race_scoreboard.insert(move(Mickel));
}
assert((race_scoreboard == decltype(race_scoreboard){
{1, "Jenti"},
{2, "Shree"},
{3, "Mickel"},
}));
return EXIT_SUCCESS;
}
std::map
to imitate the racing position. And after a while, Jenti took the lead and Mickel left behind. In this case, how we have switched the keys(position on a race track) of those players.
std::map::extract
comes in two flavours:
xxxxxxxxxx
node_type extract(const_iterator position);
node_type extract(const key_type& x);
What If the Node With a Particular Key Does Not Exist?
If we try to extract an item that doesn't exist with the second method (the one that searches using a key), it returns an emptynode_type
instance i.e. node handle. The
empty()
member method or overloaded bool operator tells us that whether a
node_type
instance is empty or not.
OK! Then How Do I Modify std::map Keys?
After extracting nodes, we were able to modify their keys using thekey()
method, which gives us non-const access to the key, although keys are usually
const.
node_type
instance, this does not result in actual moves of any of the container values.
Can I Modify Associated Values in std::map Also?
Yes! You can use the accessor methodsnh.mapped()
(instead of
nh.key()
) to manipulate the pieces of the entry in a
std::map
(or
nh.value()
for the single piece of data in an element of a
std::set
). Thus you can extract, manipulate, and reinsert a key without ever copying or moving its actual data.
But What About Safety?
If you extract a node from a map and then throw an exception before you've managed to re-insert it into the destination map.std::map::extract
by-default(without insert) will act as std::map::erase!
There Is More! Interoperability
Map nodes that have been extracted using thestd::map::extract
are actually very versatile.
We can extract nodes from a map instance and insert it into any other map or even multimap instance.
Difference Between operator[ ] vs insert() vs at()
This is trivial for experienced devs but, still I want to go over it quickly.
std::map::operator[ ]
Operation: find-or-add; try to find an element with the given key inside the map, and if it exists it will return a reference to the stored value. If it does not, it will create a new element inserted in place with default initialization and return a reference to it.- Not usable for
const std::map
, as it will create the element if it doesn't exist. - Not suitable for value type that does not default constructible and assignable(in layman term, doesn't have default constructor & copy/move constructor).
std::map::insert
Operation: insert-or-nop; accepts a value_type (std::pair
) and uses the key(first member) and to insert it. As
std::map
does not allow for duplicates, if there is an existing element it will not insert anything.
- Liberty in calling insert different ways that require the creation of the value_type externally and the copy of that object into the container.
- Highly applicable when item insertion sequence is somewhat predictable to gain the performance.
std::map::at
Operation: find-or-throw; returns a reference to the mapped value of the element with key equivalent to input key. If no such element exists, an exception of type std::out_of_range is thrown.- Not recommended using
at()
when accessing const maps and when element absence is a logic error. - Yes, it's better to use
std::map::find()
when you're not sure element is there. Because, throwing and catching std::logic_error exception will not be a very elegant way of programming, even if we don't think about performance.
Parting Words
If you see the table of content for this article above, more than half of the member functions are around inserting the elements into the map. To the newbie, this is the reason for anxiety(or standard committee would say modernness). But if you account for the new features & complexity of language those are pretty much justified. BTW, this modernness doesn't stop here, we do have other specialization also available for map like std::swap (C++17), std::erase_if (C++20) & bunch of comparison operators.
Published at DZone with permission of Vishal Chovatiya. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments