DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Migrating from React Router v5 to v6: A Comprehensive Guide
  • Playwright: Filter Visible Elements With locator.filter({ visible: true })
  • React Callback Refs: What They Are and How to Use Them
  • Process Mining Key Elements

Trending

  • Event-Driven Architectures: Designing Scalable and Resilient Cloud Solutions
  • Unlocking AI Coding Assistants Part 2: Generating Code
  • Evolution of Cloud Services for MCP/A2A Protocols in AI Agents
  • Java's Quiet Revolution: Thriving in the Serverless Kubernetes Era

Using std::map Wisely With Modern C++

In this article, see some reasons to use std::map with Modern C++.

By 
Vishal Chovatiya user avatar
Vishal Chovatiya
·
Aug. 06, 20 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
28.2K Views

Join the DZone community and get the full member experience.

Join For Free

std::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:
Java
 




x


 
1
if (auto search = freq_of.find(2); search != freq_of.end()) {
2
    cout << "Found" << endl;
3
}
4
// Where assume, freq_of = map<uint32_t, uint32_t>{{3, 1}, {1, 1}, {2, 1}};


Rather, from C++20, you can write:
Java
 




xxxxxxxxxx
1


 
1
if (freq_of.contains(2)) {
2
    cout << "Found" << endl;
3
}


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:
  1.  The key doesn't exist yet. Create a fresh key-value pair.
  2.  The key does exist already. Take the existing item and modify it.
A typical approach to insert an element in 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.
Java
 




xxxxxxxxxx
1
14


 
1
int main() {
2
    vector v{3, 4, 5, 8, 7, 3, 5, 2, 4};
3
    map<uint32_t, uint32_t> freq_of;
4

           
5
    for (const auto &n : v) {
6
        if (const auto &[it, inserted] = freq_of.emplace(n, 1); !inserted) {
7
            it->second++;  // Exists already
8
        }
9
    }
10

           
11
    assert(freq_of[3] == 2);
12

           
13
    return EXIT_SUCCESS;
14
}


Instead:
Java
 




xxxxxxxxxx
1


 
1
if (const auto &[it, inserted] = freq_of.try_emplace(n, 1); !inserted) {
2
    it->second++;
3
}


But, since C++17, there is this std::map::try_emplace method that creates items only if the key doesn't exist yet. This boosts the performance in case objects of that type are expensive to create.
 
Although the above example hasn't showcased the expensive to create items. But, yes! whenever you encounter such a situation, must be known how to handle it with 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.
 
For example, while counting the frequency of elements with the added constraint that when an element is repeated(i.e. assigned) you have to remove all the element lesser than the current one.
 
In such a situation, 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.
Java
 




xxxxxxxxxx
1
19


 
1
int main() {
2
    vector v{8, 3, 9, 5, 8};
3
    map<uint32_t, uint32_t> freq_of;
4

           
5
    for (auto &&n : v) {
6
        const auto &[it, is_inserted] = freq_of.insert_or_assign(n, 1);
7

           
8
        if (!is_inserted) { // remove all lesser element then current one if repeated
9
            freq_of.erase(begin(freq_of), it);
10
        }
11
    }
12
    
13
    assert((freq_of == decltype(freq_of){
14
                           {8, 1},
15
                           {9, 1},
16
                       }));
17

           
18
    return EXIT_SUCCESS;
19
}


std::map::insert With Hint (C++11/17)

Looking up items in an std::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.
 
In order to make this more efficient, 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.
 
This is quite useful from a performance point of view when the insertion sequence of items is somewhat predictable. For example:
Java
 




xxxxxxxxxx
1
17


 
1
int main() {
2
    map<uint32_t, string> m{{2, ""}, {3, ""}};
3
    auto where(end(m));
4

           
5
    for (const auto &n : {8, 7, 6, 5, 4, 3, 2, 1}) { // Items in non-incremental order
6
        where = m.insert(where, {n, ""});
7
    }
8

           
9
    // How it is not done!
10
    // m.insert(end(m), {0, ""});
11

           
12
    for (const auto &[key, value] : m) {
13
        cout << key << " : " << value << endl;
14
    }
15

           
16
    return EXIT_SUCCESS;
17
}


A correct hint will point to an existing element, which is greater than the element to be inserted so that the newly inserted key will be just before the hint. If this does not apply for the hint the user provided during insertion, the insert function will fall back to a nonoptimized insertion, yielding O(log(n)) performance again.
 
For the above example, the first insertion, we got the end iterator of the map, because we had no better hint to start with. After installing an 8 in the tree, we knew that installing 7 will insert a new item just in front of the 8, which qualified it to be a correct hint. This applies to 6 as well, if put into the tree after inserting the 7, and so on. This is why it is possible to use the iterator, which was returned in the last insertion for the next insertion.
 
You can play around the above example to justify the performance gain with quick-benchmark.

**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 have std::map::merge which can merge the two same type of std::map.
Java
 




xxxxxxxxxx
1
20


 
1
int main() {
2
    map<uint32_t, string> fruits{{5, "grapes"}, {2, "tomoto"}};
3
    map<uint32_t, string> person{{2, "mickel"}, {10, "shree"}};
4
    map<uint32_t, string> fruits_and_persons;
5

           
6
    fruits_and_persons.merge(fruits);
7
    assert(fruits.size() == 0);
8

           
9
    fruits_and_persons.merge(person);
10
    assert(person.size() == 1);
11
    assert(person.at(2) == "mickel"); // Won't overwrite value at 2 i.e.`mickel`
12

           
13
    assert((fruits_and_persons == decltype(fruits){
14
                                      {2, "tomoto"},
15
                                      {5, "grapes"},
16
                                      {10, "shree"},
17
                                  }));
18

           
19
    return EXIT_SUCCESS;
20
}


The thing here to note is what happens when there are duplicates! The duplicated elements are not transferred. They're left behind in the right-hand-side map.

std::map::extract (C++17)

Unlike std::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.
 
As we know, for 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.
 
This kind of restriction is perfectly valid because it makes harder for the user to use std::map the wrong way. But what if we really need to change the keys of some map items?
 
Prior to C++17, we had to remove & reinsert the items in order to change the key. The downside of this approach is memory allocation & deallocation, which sounds bad in terms of performance. But, from C++17, we can remove & reinsert std::map nodes without any reallocation of memory.
Java
 




xxxxxxxxxx
1
24


 
1
int main() {
2
    map<int, string> race_scoreboard{{1, "Mickel"}, {2, "Shree"}, {3, "Jenti"}};
3
    using Pair = map<int, string>::value_type;
4

           
5
    {
6
        auto Jenti(race_scoreboard.extract(3));
7
        auto Mickel(race_scoreboard.extract(1));
8

           
9
        swap(Jenti.key(), Mickel.key());
10

           
11
        auto [it, is_inserted, nh] = race_scoreboard.insert(move(Jenti)); // nh = node handle
12
        assert(*it == Pair(1, "Jenti") && is_inserted == true && nh.empty());
13

           
14
        race_scoreboard.insert(move(Mickel));
15
    }
16

           
17
    assert((race_scoreboard == decltype(race_scoreboard){
18
                                   {1, "Jenti"},
19
                                   {2, "Shree"},
20
                                   {3, "Mickel"},
21
                               }));
22

           
23
    return EXIT_SUCCESS;
24
}


Consider the above example of the racing scoreboard where you have employed 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:
Java
 




xxxxxxxxxx
1


 
1
node_type extract(const_iterator position);
2
node_type extract(const key_type& x);


In the above example, we used the second one, which accepts a key and then finds & extracts the map node that matches the key parameter. The first one accepts an iterator, which implies that it is faster because it doesn't need to search for the item.

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 empty node_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 the key() method, which gives us non-const access to the key, although keys are usually const.

Note that in order to reinsert the nodes into the map again, we had to move them into the insert function. This makes sense because the extract is all about avoiding unnecessary copies and allocations. Moreover, while we move a 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 methods nh.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.
 
A node handle's destructor is called and will correctly clean up the memory associated with the node. So, technically std::map::extract by-default(without insert) will act as std::map::erase!

There Is More! Interoperability

Map nodes that have been extracted using the std::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.
 
It does also work between unordered_map and unordered_multimap instances, as well as with set/ multiset and respective unordered_set/ unordered_multiset.
 
In order to move items between different map/set structures, the types of key, value and allocator need to be identical.

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.
 
Applicability:
  •   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).
When key exists: Overwrites it.

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.
 
Applicability:
  •   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.
When key exists: Not modify the state of the map, but instead return an iterator to the element that prevented the insertion.

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.
 
Applicability:
  •   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.
When key exists: returns a reference to mapped value.

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.

Element

Published at DZone with permission of Vishal Chovatiya. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Migrating from React Router v5 to v6: A Comprehensive Guide
  • Playwright: Filter Visible Elements With locator.filter({ visible: true })
  • React Callback Refs: What They Are and How to Use Them
  • Process Mining Key Elements

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!