C++ Map
Introduction
In the C++ Standard Template Library (STL), a map
is an associative container that stores elements in key-value pairs. Unlike sequential containers like vectors or arrays where elements are accessed by their position, maps allow you to access elements by their keys.
Maps in C++ have several important characteristics:
- Ordered storage: Elements are always sorted according to the key
- Unique keys: Each key can appear only once in the map
- Associative access: Elements are accessed by their keys, not by position
- Self-balancing: Maps are typically implemented as red-black trees, providing logarithmic complexity for operations
Maps are incredibly useful when you need to:
- Look up values based on a specific key
- Store data that naturally comes in key-value pairs (like dictionaries)
- Maintain a sorted collection of key-value pairs
Basic Map Operations
Creating a Map
To use maps in C++, you need to include the <map>
header:
#include <map>
#include <iostream>
#include <string>
int main() {
// Create an empty map with string keys and int values
std::map<std::string, int> ages;
// Create a map with initialization
std::map<std::string, std::string> capitals = {
{"USA", "Washington D.C."},
{"France", "Paris"},
{"Japan", "Tokyo"}
};
return 0;
}
Inserting Elements
There are several ways to insert elements into a map:
#include <map>
#include <iostream>
#include <string>
int main() {
std::map<std::string, int> ages;
// Method 1: Using the insert() function with std::pair
ages.insert(std::pair<std::string, int>("Alice", 25));
// Method 2: Using the insert() function with std::make_pair
ages.insert(std::make_pair("Bob", 30));
// Method 3: Using array-like syntax (most common)
ages["Charlie"] = 22;
// Method 4: Using emplace() (most efficient in C++11 and later)
ages.emplace("David", 28);
// Display the map content
for (const auto& pair : ages) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Output:
Alice: 25
Bob: 30
Charlie: 22
David: 28
Notice that the elements are automatically sorted alphabetically by key.
Accessing Elements
Maps provide multiple ways to access elements:
#include <map>
#include <iostream>
#include <string>
int main() {
std::map<std::string, int> scores = {
{"Alice", 95},
{"Bob", 87},
{"Charlie", 92}
};
// Method 1: Using the [] operator
std::cout << "Bob's score: " << scores["Bob"] << std::endl;
// Be careful: using [] creates a new element if the key doesn't exist
std::cout << "David's score (before): " << scores["David"] << std::endl;
// Method 2: Using at() - safer but throws exception if key doesn't exist
try {
std::cout << "Alice's score: " << scores.at("Alice") << std::endl;
std::cout << "Eve's score: " << scores.at("Eve") << std::endl; // This will throw an exception
} catch (const std::out_of_range& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
// Method 3: Using find() - safest approach
auto it = scores.find("Charlie");
if (it != scores.end()) {
std::cout << "Charlie's score: " << it->second << std::endl;
} else {
std::cout << "Charlie not found!" << std::endl;
}
// Print the map after operations
std::cout << "\nFinal map contents:" << std::endl;
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Output:
Bob's score: 87
David's score (before): 0
Alice's score: 95
Exception: map::at
Charlie's score: 92
Final map contents:
Alice: 95
Bob: 87
Charlie: 92
David: 0
Using the []
operator creates a new element with a default value if the key doesn't exist. This can lead to unexpected behavior. Use at()
or find()
when you only want to check if an element exists.
Checking If an Element Exists
Before accessing elements, it's often important to check if a key exists:
#include <map>
#include <iostream>
#include <string>
int main() {
std::map<std::string, int> ages = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 22}
};
// Method 1: Using count() - returns 1 if key exists, 0 otherwise
if (ages.count("Bob") > 0) {
std::cout << "Bob exists in the map" << std::endl;
}
// Method 2: Using find() - returns iterator to element if found, or end() otherwise
if (ages.find("David") != ages.end()) {
std::cout << "David exists in the map" << std::endl;
} else {
std::cout << "David does not exist in the map" << std::endl;
}
// Method 3: Using contains() (C++20 and later)
// if (ages.contains("Alice")) {
// std::cout << "Alice exists in the map" << std::endl;
// }
return 0;
}
Output:
Bob exists in the map
David does not exist in the map
Removing Elements
You can remove elements from a map in several ways:
#include <map>
#include <iostream>
#include <string>
int main() {
std::map<std::string, int> scores = {
{"Alice", 95},
{"Bob", 87},
{"Charlie", 92},
{"David", 78},
{"Eve", 85}
};
// Method 1: Using erase() with a key
scores.erase("David");
// Method 2: Using erase() with an iterator
auto it = scores.find("Eve");
if (it != scores.end()) {
scores.erase(it);
}
// Method 3: Using erase() with a range of iterators
auto start = scores.find("Alice");
auto end = scores.find("Charlie");
if (start != scores.end() && end != scores.end()) {
// This will erase from start to end (not including end)
scores.erase(start, end);
}
// Print the remaining elements
std::cout << "Remaining scores:" << std::endl;
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// Clear the entire map
scores.clear();
std::cout << "Size after clear(): " << scores.size() << std::endl;
return 0;
}
Output:
Remaining scores:
Charlie: 92
Size after clear(): 0
Map Properties and Methods
Maps provide various methods to work with their contents:
#include <map>
#include <iostream>
#include <string>
int main() {
std::map<std::string, int> scores = {
{"Alice", 95},
{"Bob", 87},
{"Charlie", 92},
{"David", 78}
};
// Check if map is empty
std::cout << "Is map empty? " << (scores.empty() ? "Yes" : "No") << std::endl;
// Get the size of the map
std::cout << "Map size: " << scores.size() << std::endl;
// Get the maximum possible size
std::cout << "Maximum size: " << scores.max_size() << std::endl;
// Swap two maps
std::map<std::string, int> other_scores = {{"Eve", 91}, {"Frank", 84}};
scores.swap(other_scores);
std::cout << "\nAfter swap:" << std::endl;
std::cout << "scores map:" << std::endl;
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
std::cout << "\nother_scores map:" << std::endl;
for (const auto& pair : other_scores) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Output:
Is map empty? No
Map size: 4
Maximum size: 461168601842738790
After swap:
scores map:
Eve: 91
Frank: 84
other_scores map:
Alice: 95
Bob: 87
Charlie: 92
David: 78
Working with Map Iterators
Maps provide iterators for traversing elements:
#include <map>
#include <iostream>
#include <string>
int main() {
std::map<std::string, int> scores = {
{"Alice", 95},
{"Bob", 87},
{"Charlie", 92},
{"David", 78}
};
// Forward iteration
std::cout << "Forward iteration:" << std::endl;
for (auto it = scores.begin(); it != scores.end(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
// Reverse iteration
std::cout << "\nReverse iteration:" << std::endl;
for (auto it = scores.rbegin(); it != scores.rend(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
// Modern range-based for loop (C++11 and later)
std::cout << "\nRange-based for loop:" << std::endl;
for (const auto& [key, value] : scores) { // C++17 structured binding
std::cout << key << ": " << value << std::endl;
}
return 0;
}
Output:
Forward iteration:
Alice: 95
Bob: 87
Charlie: 92
David: 78
Reverse iteration:
David: 78
Charlie: 92
Bob: 87
Alice: 95
Range-based for loop:
Alice: 95
Bob: 87
Charlie: 92
David: 78
Custom Key Comparison
By default, maps use std::less<Key>
for ordering elements. You can provide a custom comparison function:
#include <map>
#include <iostream>
#include <string>
#include <functional> // For std::greater
int main() {
// A map with case-insensitive string comparison
struct CaseInsensitiveCompare {
bool operator()(const std::string& a, const std::string& b) const {
std::string a_lower = a, b_lower = b;
// Convert both strings to lowercase
std::transform(a.begin(), a.end(), a_lower.begin(),
[](unsigned char c) { return std::tolower(c); });
std::transform(b.begin(), b.end(), b_lower.begin(),
[](unsigned char c) { return std::tolower(c); });
return a_lower < b_lower;
}
};
// Map with custom comparison
std::map<std::string, int, CaseInsensitiveCompare> scores;
scores["Alice"] = 95;
scores["bob"] = 87; // Note lowercase "bob"
scores["Charlie"] = 92;
// This won't create a new entry, but update "bob"
scores["BOB"] = 88; // Note uppercase "BOB"
std::cout << "Case-insensitive map:" << std::endl;
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// Map with reverse ordering (greater instead of less)
std::map<int, std::string, std::greater<int>> scores_desc;
scores_desc[95] = "Alice";
scores_desc[87] = "Bob";
scores_desc[92] = "Charlie";
std::cout << "\nReverse-ordered map:" << std::endl;
for (const auto& pair : scores_desc) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Output:
Case-insensitive map:
Alice: 95
bob: 88
Charlie: 92
Reverse-ordered map:
95: Alice
92: Charlie
87: Bob
Map Performance Characteristics
The map container in C++ is typically implemented as a balanced binary search tree (usually a red-black tree), which offers the following performance characteristics:
Practical Applications
Let's explore some practical applications of maps:
Word Frequency Counter
#include <map>
#include <iostream>
#include <string>
#include <sstream>
#include <algorithm>
int main() {
// Sample text
std::string text = "the quick brown fox jumps over the lazy dog the fox is quick";
std::map<std::string, int> word_count;
std::stringstream ss(text);
std::string word;
// Count word frequencies
while (ss >> word) {
// Convert to lowercase
std::transform(word.begin(), word.end(), word.begin(),
[](unsigned char c) { return std::tolower(c); });
// Increment count for this word
word_count[word]++;
}
// Display word frequencies
std::cout << "Word frequencies:" << std::endl;
for (const auto& [word, count] : word_count) {
std::cout << word << ": " << count << std::endl;
}
// Find the most frequent word
auto max_element = std::max_element(
word_count.begin(), word_count.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
}
);
std::cout << "\nMost frequent word: " << max_element->first
<< " (" << max_element->second << " occurrences)" << std::endl;
return 0;
}
Output:
Word frequencies:
brown: 1
dog: 1
fox: 2
is: 1
jumps: 1
lazy: 1
over: 1
quick: 2
the: 3
Most frequent word: the (3 occurrences)
Student Record System
#include <map>
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
struct Student {
std::string name;
int age;
std::vector<std::pair<std::string, char>> courses; // course name, grade
// Add a course with grade
void addCourse(const std::string& course, char grade) {
courses.push_back({course, grade});
}
// Calculate GPA (A=4, B=3, C=2, D=1, F=0)
double getGPA() const {
if (courses.empty()) return 0.0;
double total = 0.0;
for (const auto& course : courses) {
switch (course.second) {
case 'A': total += 4.0; break;
case 'B': total += 3.0; break;
case 'C': total += 2.0; break;
case 'D': total += 1.0; break;
case 'F': total += 0.0; break;
}
}
return total / courses.size();
}
};
int main() {
// Create a map of students using ID as key
std::map<std::string, Student> students;
// Add some students
students["S001"] = {"Alice Smith", 20, {}};
students["S001"].addCourse("Math", 'A');
students["S001"].addCourse("Physics", 'B');
students["S001"].addCourse("Programming", 'A');
students["S002"] = {"Bob Johnson", 22, {}};
students["S002"].addCourse("History", 'B');
students["S002"].addCourse("Literature", 'A');
students["S002"].addCourse("Economics", 'C');
students["S003"] = {"Charlie Brown", 19, {}};
students["S003"].addCourse("Chemistry", 'B');
students["S003"].addCourse("Biology", 'A');
students["S003"].addCourse("Math", 'C');
// Display student records
std::cout << "Student Records:" << std::endl;
std::cout << std::setfill('-') << std::setw(60) << "-" << std::endl;
std::cout << std::setfill(' ');
std::cout << std::left << std::setw(10) << "ID"
<< std::setw(20) << "Name"
<< std::setw(10) << "Age"
<< std::setw(10) << "GPA"
<< "Courses" << std::endl;
std::cout << std::setfill('-') << std::setw(60) << "-" << std::endl;
std::cout << std::setfill(' ');
for (const auto& [id, student] : students) {
std::cout << std::left << std::setw(10) << id
<< std::setw(20) << student.name
<< std::setw(10) << student.age
<< std::setw(10) << std::fixed << std::setprecision(2) << student.getGPA();
for (const auto& [course, grade] : student.courses) {
std::cout << course << "(" << grade << ") ";
}
std::cout << std::endl;
}
// Look up a student by ID
std::string lookup_id = "S002";
auto it = students.find(lookup_id);
if (it != students.end()) {
std::cout << "\nFound student: " << it->second.name
<< " (GPA: " << it->second.getGPA() << ")" << std::endl;
} else {
std::cout << "\nStudent with ID " << lookup_id << " not found." << std::endl;
}
return 0;
}
Output:
Student Records:
------------------------------------------------------------
ID Name Age GPA Courses
------------------------------------------------------------
S001 Alice Smith 20 3.33 Math(A) Physics(B) Programming(A)
S002 Bob Johnson 22 2.67 History(B) Literature(A) Economics(C)
S003 Charlie Brown 19 2.67 Chemistry(B) Biology(A) Math(C)
Found student: Bob Johnson (GPA: 2.67)
Multimap: When You Need Duplicate Keys
The STL also provides std::multimap
, which allows multiple elements with the same key:
#include <map>
#include <iostream>
#include <string>
int main() {
// Create a multimap
std::multimap<std::string, std::string> phoneBook;
// Insert multiple entries with the same key
phoneBook.insert({"Smith", "555-1234"});
phoneBook.insert({"Johnson", "555-5678"});
phoneBook.insert({"Smith", "555-8765"}); // Another number for Smith
phoneBook.insert({"Smith", "555-9999"}); // Yet another number for Smith
// Display all entries
std::cout << "Phone Book:" << std::endl;
for (const auto& entry : phoneBook) {
std::cout << entry.first << ": " << entry.second << std::endl;
}
// Count how many entries Smith has
std::string name = "Smith";
std::cout << "\n" << name << " has " << phoneBook.count(name) << " phone numbers" << std::endl;
// Find all entries for Smith
std::cout << "\nAll numbers for " << name << ":" << std::endl;
auto range = phoneBook.equal_range(name);
for (auto it = range.first; it != range.second; ++it) {
std::cout << it->second << std::endl;
}
return 0;
}
Output:
Phone Book:
Johnson: 555-5678
Smith: 555-1234
Smith: 555-8765
Smith: 555-9999
Smith has 3 phone numbers
All numbers for Smith:
555-1234
555-8765
555-9999
Unordered Maps: When Order Doesn't Matter
If you don't need ordered keys, consider using std::unordered_map
for potentially better performance:
- std::map
- std::unordered_map
#include <map>
#include <iostream>
#include <string>
#include <chrono>
int main() {
// Using regular map (ordered)
std::map<int, int> orderedMap;
// Insert 100,000 elements
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; i++) {
orderedMap[i] = i * 2;
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
std::cout << "Time to insert 100,000 elements in std::map: "
<< elapsed.count() << " ms" << std::endl;
// Lookup test
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; i++) {
auto val = orderedMap[i];
}
end = std::chrono::high_resolution_clock::now();
elapsed = end - start;
std::cout << "Time to lookup 100,000 elements in std::map: "
<< elapsed.count() << " ms" << std::endl;
return 0;
}
#include <unordered_map>
#include <iostream>
#include <string>
#include <chrono>
int main() {
// Using unordered_map (hash-based)
std::unordered_map<int, int> unorderedMap;
// Insert 100,000 elements
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; i++) {
unorderedMap[i] = i * 2;
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
std::cout << "Time to insert 100,000 elements in std::unordered_map: "
<< elapsed.count() << " ms" << std::endl;
// Lookup test
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; i++) {
auto val = unorderedMap[i];
}
end = std::chrono::high_resolution_clock::now();
elapsed = end - start;
std::cout << "Time to lookup 100,000 elements in std::unordered_map: "
<< elapsed.count() << " ms" << std::endl;
return 0;
}
Typical Output:
Time to insert 100,000 elements in std::map: 82.3 ms
Time to lookup 100,000 elements in std::map: 74.5 ms
Time to insert 100,000 elements in std::unordered_map: 31.2 ms
Time to lookup 100,000 elements in std::unordered_map: 12.7 ms
Use std::map
when:
- You need keys to be sorted
- You need to find the next element greater/less than a value
- You need to iterate in a specific order
Use std::unordered_map
when:
- You only need fast lookups
- Order doesn't matter
- Memory usage is less of a concern (unordered_map can use more memory)
Summary
The std::map
container in C++ provides a powerful and flexible way to store key-value pairs with several important features:
- Keys are always maintained in sorted order
- Each key appears exactly once (unless you use
std::multimap
) - Access, insertion, and deletion operations are logarithmic in complexity (O(log n))
- Maps are implemented as self-balancing binary search trees
Maps are exceptionally useful for:
- Dictionary-like data structures
- Frequency counting
- Index lookups
- Caching results
- Any problem requiring efficient key-value associations
Additional Resources and Exercises
Resources
Exercises
-
Word Replacement: Create a program that replaces specific words in a text with others based on a map of substitutions.
-
Simple Database: Implement a simple database system using maps that can:
- Add records with a unique ID
- Delete records by ID
- Update existing records
- Search for records by different fields
-
Cache Implementation: Create a simple caching system using a map that stores results of "expensive" operations and retrieves them when the same input is provided again.
-
Map vs. Unordered Map: Write a program that compares the performance of
std::map
andstd::unordered_map
for different operations (insertion, deletion, lookup) with various numbers of elements. -
Nested Maps: Create a nested map structure to represent a directory system, where each directory can contain files (with their sizes) and other directories.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)