std::unordered_map<Key, T>, std::unordered_multimap<Key, T>: Associative containers that contain key-value pairs (maps) and allow multiple elements with the same key (multimaps).std::unordered_set<Key>, std::unordered_multiset<Key>: Associative containers that contain unique keys (sets) and allow multiple elements with the same key (multisets).std::hash<Key> or provide a custom hash function).std::map#include <map>
std::map<std::string, int> ages;
ages["Alice"] = 25;
ages["Bob"] = 30;
ages.insert({"Charlie", 35});
// Access elements.
std::cout << ages["Alice"] << std::endl; // 25
std::cout << ages.at("Bob") << std::endl; // 30
// Iterate over elements (sorted by key).
for (const auto& [name, age] : ages) {
std::cout << name << ": " << age << std::endl;
}
std::set#include <set>
std::set<int> numbers = {5, 2, 8, 2, 1};
// Automatically sorted and duplicates removed: {1, 2, 5, 8}
numbers.insert(3); // {1, 2, 3, 5, 8}
numbers.erase(2); // {1, 3, 5, 8}
// Check if element exists.
if (numbers.find(5) != numbers.end()) {
std::cout << "5 is in the set" << std::endl;
}
std::cout << numbers.count(3) << std::endl; // Either 1 or 0.
// Iterate over elements (sorted).
for (int num : numbers) {
std::cout << num << " ";
}
std::unordered_map#include <unordered_map>
std::unordered_map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
scores["Charlie"] = 92;
// Fast average O(1) lookup.
std::cout << scores["Alice"] << std::endl; // 95
// Iterate (order not guaranteed).
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << std::endl;
}
| Operation | vector |
list |
deque |
map |
unordered_map |
|---|---|---|---|---|---|
| Random access | |||||
| Insert/delete at end | |||||
| Insert/delete at beginning | |||||
| Insert/delete in middle | |||||
| Search |
Note:
list and map have higher per-element memory overhead due to pointers/nodes.
Container adaptors provide restricted interfaces to underlying containers:
std::stack<T>: LIFO (Last In, First Out) structure.
push(), pop(), top().std::deque<T>.std::queue<T>: FIFO (First In, First Out) structure.
push(), pop(), front(), back().std::deque<T>.std::priority_queue<T>: Elements retrieved based on priority (largest element first by default).
push(), pop(), top().std::vector<T>.std::stack#include <stack>
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
std::cout << s.top() << std::endl; // 3
s.pop();
std::cout << s.top() << std::endl; // 2
std::priority_queue#include <queue>
std::priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(50);
pq.push(20);
while (!pq.empty()) {
std::cout << pq.top() << " "; // 50 30 20 10
pq.pop();
}
std::pair<T1, T2>: Stores two heterogeneous values.
std::pair<int, std::string> p{1, "one"};
std::cout << p.first << ", " << p.second << std::endl;
std::tuple<T1, T2, ...>: Stores multiple heterogeneous values.
std::tuple<int, std::string, double> t{1, "one", 1.0};
std::cout << std::get<0>(t) << std::endl; // 1
std::optional<T> (C++17): May or may not contain a value.
Useful for dealing with datasets with missing data.
std::optional<int> opt = 42;
if (opt.has_value()) std::cout << *opt << std::endl;
std::variant<T1, T2, ...> (C++17): Type-safe union.std::variant<int, double, std::string> v = 42;
v = 3.14;
v = "hello";
// Check which type is currently held.
if (std::holds_alternative<std::string>(v)) {
std::cout << "Holds string: " << std::get<std::string>(v) << std::endl;
}
// Safe access using std::get_if (returns nullptr if wrong type).
if (auto* ptr = std::get_if<double>(&value)) {
std::cout << "Double: " << *ptr << std::endl;
}
// Use std::visit to handle all possible types.
std::visit([](const auto &arg) {
std::cout << "Value: " << arg << std::endl;
}, v);
std::any (C++17): Can hold any type.std::any a = 42;
a = std::string("hello");
// Check type and extract value safely.
if (a.has_value()) {
if (a.type() == typeid(std::string)) {
std::cout << std::any_cast<std::string>(a) << std::endl;
}
}
// Direct cast (throws exception if wrong type).
std::cout << std::any_cast<std::string>(a) << std::endl;
Iterators are objects that allow traversal through the elements of a container. They act as a bridge between containers and algorithms, providing a uniform interface for accessing elements.
std::list).std::vector).std::vector<int> vec = {1, 2, 3, 4, 5};
// Using iterators.
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// Using const iterators.
for (std::vector<int>::const_iterator it = vec.cbegin();
it != vec.cend(); ++it) {
std::cout << *it << " ";
}
// Reverse iterators.
for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
std::cout << *rit << " "; // 5 4 3 2 1
}
std::vector<int> vec = {10, 20, 30, 40, 50};
auto it = vec.begin();
std::cout << *it << std::endl; // 10 (dereference)
++it; // Move to next element.
std::cout << *it << std::endl; // 20
it += 2; // Jump forward (random access only).
std::cout << *it << std::endl; // 40
auto distance = std::distance(vec.begin(), it); // 3
std::cout << distance << std::endl;
std::advance(it, 1); // Move forward by 1.
std::cout << *it << std::endl; // 50
Modifying containers can invalidate iterators:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.erase(it); //
Invalidates 'it'!
// Correct: it = vec.erase(it);
}
}
Rule: After modifying a container, update or re-obtain iterators.
The <algorithm> header provides a rich collection of functions for:
find, binary_search, lower_bound, upper_boundsort, stable_sort, partial_sortcopy, move, transform, replace, fillremove, remove_if, uniquepartition, stable_partitionmin, max, min_element, max_elementaccumulate, inner_product, partial_sum (in <numeric>) Most algorithms work with iterator ranges
[begin, end).
Non-modifying algorithms: Inspect but don't change elements
find, count, all_of, any_of, none_ofModifying algorithms: Change elements or container structure
copy, transform, replace, remove, reverseSorting algorithms: Order elements
sort, stable_sort, partial_sort, nth_elementSet algorithms: Operate on sorted ranges
set_union, set_intersection, set_differenceNumeric algorithms: Mathematical operations (in <numeric>)
accumulate, inner_product, adjacent_difference#include <algorithm>
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
// find: Linear search.
auto it = std::find(vec.begin(), vec.end(), 3);
if (it != vec.end()) {
std::cout << "Found: " << *it << std::endl;
}
// count: Count occurrences.
int count = std::count(vec.begin(), vec.end(), 3);
std::cout << "Count: " << count << std::endl;
// binary_search: Requires sorted range.
bool found = std::binary_search(vec.begin(), vec.end(), 3);
std::cout << "Binary search: " << found << std::endl;
#include <algorithm>
#include <vector>
std::vector<int> vec = {5, 2, 8, 1, 9};
// sort: Average O(n log n).
std::sort(vec.begin(), vec.end());
// vec is now: {1, 2, 5, 8, 9}
// Sort in descending order.
std::sort(vec.begin(), vec.end(), std::greater<int>());
// vec is now: {9, 8, 5, 2, 1}
// Custom comparator.
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return std::abs(a) < std::abs(b);
});
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> result(5);
// copy: Copy elements.
std::copy(vec.begin(), vec.end(), result.begin());
// transform: Apply function to each element.
std::transform(vec.begin(), vec.end(), result.begin(),
[](int x) { return x * 2; });
// result is now: {2, 4, 6, 8, 10}
// replace: Replace all occurrences.
std::replace(vec.begin(), vec.end(), 3, 99);
// vec is now: {1, 2, 99, 4, 5}
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
// remove: Moves elements to end, returns new logical end.
auto new_end = std::remove(vec.begin(), vec.end(), 2);
vec.erase(new_end, vec.end()); // Actually remove them.
// vec is now: {1, 3, 4, 5}
// remove_if: Remove elements satisfying predicate.
vec = {1, 2, 3, 4, 5, 6};
new_end = std::remove_if(vec.begin(), vec.end(),
[](int x) { return x % 2 == 0; });
vec.erase(new_end, vec.end());
// vec is now: {1, 3, 5}
#include <numeric>
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
// accumulate: Sum of elements.
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << "Sum: " << sum << std::endl; // 15
// Product of elements.
int product = std::accumulate(vec.begin(), vec.end(), 1,
std::multiplies<int>());
std::cout << "Product: " << product << std::endl; // 120
// inner_product: Dot product.
std::vector<int> vec2 = {1, 2, 3, 4, 5};
int dot = std::inner_product(vec.begin(), vec.end(), vec2.begin(), 0);
std::cout << "Dot product: " << dot << std::endl; // 55
Comparing manual loops vs STL algorithms:
// Manual loop (error-prone, verbose).
std::vector<int> vec = {1, 2, 3, 4, 5};
int sum = 0;
for (size_t i = 0; i < vec.size(); ++i) {
sum += vec[i];
}
// STL algorithm (concise, expressive).
int sum = std::accumulate(vec.begin(), vec.end(), 0);
Benefits: More readable, less error-prone, potentially optimized, supports parallel execution (C++17).
Many algorithms support execution policies for parallel/vectorized execution:
#include <execution>
#include <algorithm>
std::vector<int> vec(1'000'000);
std::iota(vec.begin(), vec.end(), 0);
// Sequential execution.
std::sort(std::execution::seq, vec.begin(), vec.end()); // Same as: std::sort(vec.begin(), vec.end()).
// Parallel execution.
std::sort(std::execution::par, vec.begin(), vec.end());
// Parallel + vectorized execution.
std::sort(std::execution::par_unseq, vec.begin(), vec.end());
For this course, prioritize mastering these algorithms:
std::sort: Sorting elementsstd::find / std::find_if: Searching for elementsstd::copy: Copying elements between containersstd::transform: Applying functions to rangesstd::accumulate: Summing or reducing elementsstd::count / std::count_if: Counting elementsstd::remove / std::remove_if: Removing elementsstd::for_each: Applying function to each elementstd::max_element / std::min_element: Finding extremastd::reverse: Reversing element orderIterator invalidation: Modifying containers while iterating
for (auto it = vec.begin(); it != vec.end(); ++it) {
vec.push_back(42); //
Invalidates iterators!
}
Copying large containers: Use references or move semantics
void process(std::vector<int> v); //
Copies entire vector.
void process(const std::vector<int>& v); //
No copy.
Using [] on maps: Creates element if key doesn't exist
std::map<int, std::string> m;
std::cout << m[1]; //
Creates entry with key 1.
// Use m.at(1) or m.find(1) for safer access.
Forgetting to erase after remove
vec.remove(vec.begin(), vec.end(), 5); //
Doesn't actually remove.
vec.erase(std::remove(vec.begin(), vec.end(), 5), vec.end()); //
Comparing iterators from different containers
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {1, 2, 3};
if (v1.begin() == v2.begin()) { /*
Undefined behavior! */ }
Not checking return values
auto it = map.find(key);
std::cout << it->second; //
Check 'if (it != map.end())' first!
vector, map, set, arraysort, find, transform, copy, accumulateunique_ptr, shared_ptrforward_list, dequefor loops (1/5)for loop (pre-C++11)std::vector<int> vec = {1, 2, 3, 4, 5};
for (size_t i = 0; i < vec.size(); ++i) {
std::cout << vec[i] << " ";
}
std::vector or std::array).size() each time.for loops (2/5)for loop with iterators (pre-C++11)std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::list).*it) can make code harder to read.for loops (3/5)for loop (C++11)std::vector<int> vec = {1, 2, 3, 4, 5};
for (int value : vec) { // Or: for (const auto &value : vec)
std::cout << value << " ";
}
for (int &value : vec)).for loops (4/5)std::tuple<int, std::string, double> my_tuple{1, "a string", 2.5};
const auto [i, s, d] = my_tuple; // Unpack tuple.
std::map<int, std::string> my_map = {{1, "one"}, {2, "two"}};
for (const auto& [key, value] : my_map) {
std::cout << key << ": " << value << "\n";
}
std::map and std::unordered_map.std::vector.for loops (5/5)for with std::ranges (C++20)#include <ranges>
for (int value : vec | std::views::reverse) {
std::cout << value << " ";
}
reverse, filter, transform) directly in the loop using range adaptors.Try refactoring this code to use STL algorithms:
// Before: Manual loop
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> evens;
for (int n : nums) {
if (n % 2 == 0) {
evens.push_back(n * 2);
}
}
Use std::copy_if with std::back_inserter and std::transform.
Or, if you have a C++20-ready compiler, use std::ranges::copy_if with std::views::filter and std::views::transform.
The STL is a fundamental part of the C++ standard library, offering a rich set of data structures, algorithms, and utilities that make C++ a powerful and expressive language. To fully harness the power of the STL:
std::vector, std::map, std::queue) based on your specific needs. This decision greatly impacts your code's efficiency.std::shared_ptr and std::unique_ptr are crucial for effective memory management, preventing memory leaks and resource leaks.