5 The C++ Standard Library
Containers
The STL provides a variety of containers to store and manage collections of elements. Different containers have different properties — choosing the right one is essential for performance.
| Container | Structure | Access | Insert/Delete |
|---|---|---|---|
array |
fixed array | O(1) random | — |
vector |
dynamic array | O(1) random | O(1) back, O(n) middle |
deque |
double-ended queue | O(1) random | O(1) front & back |
list |
doubly linked list | O(n) | O(1) anywhere |
forward_list |
singly linked list | O(n) | O(1) after iterator |
set |
sorted BST | O(log n) | O(log n) |
unordered_set |
hash table | O(1) avg | O(1) avg |
map |
sorted BST | O(log n) | O(log n) |
unordered_map |
hash table | O(1) avg | O(1) avg |
However this chart is kind of a lie since it does not account for cache coherence and memory locality. For example, a vector is usually much faster than a list for iterating over elements, even though the list has O(1) access time for each element. Thats why there is almost no reason to use a list in modern C++. Even inserting and deleting elements in a vector is usually faster than in a list, because the vector is more cache friendly. Only use a list if you constantly need to insert and delete elements from a very large collection of elements and you don’t care about iterating over them.
Iterators allow algorithms to work on any container uniformly, independent of element layout in memory.
list and forward_list
list implements a doubly linked list: elements are not contiguous in memory, so pointer arithmetic doesn’t work, but insertion and deletion anywhere are O(1).
#include <iostream>
#include <list>
using namespace std;
int main() {
list<string> poem;
poem.push_back("i"); // [i]
auto it = poem.begin();
poem.insert(it, "ends"); // [ends, i]
poem.insert(it, "with"); // [ends, with, i]
poem.push_front("pi"); // [pi, ends, with, i]
for (const string& word : poem)
cout << word << ' '; // pi ends with i
}forward_list is a singly linked list — slightly more memory-efficient, but can only iterate forward. It uses insert_after instead of insert.
#include <forward_list>
forward_list<int> fl = {1, 2, 3};
fl.push_front(0);
fl.insert_after(fl.begin(), 10); // {0, 10, 1, 2, 3}- Advantage: Fast O(1) insertion and deletion at any position.
- Disadvantage: No random access — can’t do
fl[2].
set and unordered_set
set stores unique elements in sorted order, typically implemented as a red-black tree. Elements cannot be mutated in-place.
#include <iostream>
#include <set>
using namespace std;
int main() {
set<string> s = {"pi", "ends", "with", "i", "and", "not", "with", "y"};
// "with" is only inserted once — sets store unique elements
for (const string& word : s)
cout << word << ' '; // and ends i not pi with y (sorted!)
}unordered_set uses a hash table instead of a tree — O(1) average lookup but no guaranteed ordering.
#include <unordered_set>
unordered_set<int> us = {3, 1, 4, 1, 5, 9};
// stores {1, 3, 4, 5, 9} in unspecified order
cout << us.count(4); // 1 (found)
cout << us.count(7); // 0 (not found)Use set when you need sorted iteration. Use unordered_set when you only need fast membership checks.
map and unordered_map
map stores unique key-value pairs in sorted key order, implemented as a red-black tree.
#include <iostream>
#include <map>
#include <vector>
using namespace std;
int main() {
map<string, int> wordcount;
vector<string> poem = {"pi", "ends", "with", "i", "and", "not", "with", "y"};
for (const string& word : poem)
++wordcount[word]; // inserts 0 if key not present, then increments
// classic iterator access
for (auto it = wordcount.begin(); it != wordcount.end(); ++it)
cout << it->first << ": " << it->second << "\n";
// modern structured binding (C++17)
for (const auto& [key, value] : wordcount)
cout << key << ": " << value << "\n";
}unordered_map uses hashing for O(1) average access — prefer it when ordering doesn’t matter.
#include <unordered_map>
unordered_map<string, int> scores;
scores["alice"] = 95;
scores["bob"] = 87;
cout << scores["alice"]; // 95pair, tuple, and optional
pair groups two values of (possibly different) types:
#include <utility>
pair<int, string> id_name = make_pair(1, "bob");
cout << id_name.second << " has ID " << id_name.first << "\n";
// C++17 structured binding
auto [num, name] = id_name;tuple generalizes pair to any fixed number of elements:
#include <tuple>
// chess move: piece, from-col, from-row, to-col, to-row
tuple<char, char, int, char, int> move('K', 'c', 1, 'd', 2);
cout << "Piece: " << get<0>(move) << "\n";
// structured binding (C++17)
auto [piece, fc, fr, tc, tr] = move;optional avoids sentinel values like -1 or nullptr for “no result”:
#include <optional>
optional<int> find_index(const vector<int>& v, int x) {
for (int i = 0; i < (int)v.size(); ++i)
if (v[i] == x) return i;
return nullopt;
}
if (auto idx = find_index(v, 30); idx.has_value())
cout << "Found at index " << *idx << "\n";Iterators
An iterator is an object that abstracts traversal over a container’s elements — similar to a pointer, but works for any container regardless of how elements are laid out in memory.
All STL containers provide iterators via .begin() and .end(). begin() points to the first element; end() points one past the last (a sentinel).
vector<int> v = {2, 3, 7, 11, 13};
for (auto it = v.begin(); it != v.end(); ++it)
cout << *it << ' ';
// same pattern works for set — no index-based access possible here
set<string> s = {"hello", "cruel", "world"};
for (auto it = s.begin(); it != s.end(); ++it)
cout << *it << ' ';Range-based for loops are syntactic sugar over iterators:
for (auto num : v) { cout << num; }
// expands to:
for (auto it = v.begin(); it != v.end(); ++it) {
auto num = *it;
cout << num;
}Iterator Categories
There are five iterator categories, each a superset of the previous:
| # | Category | Extra Operations | Supported by |
|---|---|---|---|
| 1 | Input | =*it, ++it |
istream_iterator |
| 2 | Output | *it=, ++it |
ostream_iterator |
| 3 | Forward | *, ->, ++, ==, != |
forward_list, unordered_set |
| 4 | Bidirectional | adds -- |
list, map, set |
| 5 | Random Access | adds +, -, [], <, > |
array, vector, string, deque |
Adapters (stack, queue, priority_queue) expose no iterators at all.
Algorithms declare which category they require: - find, count → Input Iterator - replace, min_element → Forward Iterator - reverse → Bidirectional Iterator - sort, random_shuffle → Random Access Iterator
Reverse and const Iterators
Containers also provide rbegin()/rend() for reverse traversal and cbegin()/cend() for read-only access:
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it)
cout << *it << ' '; // 5 4 3 2 1
// const iterator — cannot modify elements
for (auto it = v.cbegin(); it != v.cend(); ++it)
cout << *it << ' ';Algorithms
The <algorithm> header provides over 100 generic algorithms that operate on iterator ranges, independent of the container type.
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = {5, 3, 8, 1, 9, 2, 7};
// searching
auto it = find(v.begin(), v.end(), 8);
if (it != v.end())
cout << "found 8 at index " << distance(v.begin(), it) << '\n';
cout << "min: " << *min_element(v.begin(), v.end()) << '\n';
cout << "max: " << *max_element(v.begin(), v.end()) << '\n';
cout << "count of 3: " << count(v.begin(), v.end(), 3) << '\n';
// sorting
sort(v.begin(), v.end());
for (int x : v) cout << x << ' '; // 1 2 3 5 7 8 9
}// transforming and filtering
reverse(v.begin(), v.end());
replace(v.begin(), v.end(), 5, 50);
// erase-remove idiom — removes all 4s in-place
v.erase(remove(v.begin(), v.end(), 4), v.end());
// copy elements matching a predicate into another container
vector<int> evens;
copy_if(v.begin(), v.end(), back_inserter(evens),
[](int x) { return x % 2 == 0; });Sorting with Custom Comparators
sort accepts a comparison function or lambda as a third argument:
vector<string> words = {"banana", "fig", "apple", "cherry", "kiwi"};
// sort by length, then alphabetically within same length
sort(words.begin(), words.end(), [](const string& a, const string& b) {
if (a.size() != b.size()) return a.size() < b.size();
return a < b;
});
// fig kiwi apple banana cherryTuples
tuple is a fixed-size heterogeneous collection. Beyond basic usage, tuples support lexicographic comparison and concatenation via tuple_cat.
A common use case is returning multiple values from a function cleanly:
#include <tuple>
#include <string>
using namespace std;
tuple<bool, int, string> parse_result(const string& input) {
if (input.empty()) return {false, 0, "empty input"};
return {true, (int)input.size(), input};
}
int main() {
auto [ok, len, text] = parse_result("hello");
if (ok) cout << '"' << text << "\" (" << len << " chars)\n";
// tuple comparison is lexicographic
auto t1 = make_tuple(1, "abc");
auto t2 = make_tuple(1, "xyz");
cout << (t1 < t2 ? "t1 < t2" : "t1 >= t2") << '\n';
// concatenate two tuples
auto t3 = tuple_cat(make_tuple(1, 2), make_tuple("three", 4.0));
cout << get<2>(t3) << '\n'; // "three"
}Function Objects
A function object (or functor) is any type that overloads operator(). They can carry state unlike plain function pointers, and the compiler can inline them efficiently.
#include <vector>
#include <algorithm>
using namespace std;
struct Adder {
int n;
Adder(int n) : n(n) {}
int operator()(int x) const { return x + n; }
};
int main() {
vector<int> v = {1, 2, 3, 4, 5};
transform(v.begin(), v.end(), v.begin(), Adder(10));
// v = {11, 12, 13, 14, 15}
}Lambda Expressions
Lambdas (C++11) are anonymous function objects defined inline. They can capture variables from the enclosing scope:
[capture](parameters) -> return_type { body }
| Capture | Meaning |
|---|---|
[] |
capture nothing |
[=] |
capture all by value |
[&] |
capture all by reference |
[x, &y] |
x by value, y by reference |
int threshold = 5;
vector<int> v = {1, 7, 3, 9, 2, 6, 4};
// capture threshold by value
auto big = count_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; });
// accumulate sum by reference capture
int total = 0;
for_each(v.begin(), v.end(), [&total](int x) { total += x; });
// sort descending
sort(v.begin(), v.end(), [](int a, int b) { return a > b; });Standard Function Objects (<functional>)
The STL ships ready-made function objects for common operations:
#include <functional>
#include <numeric>
sort(v.begin(), v.end(), greater<int>()); // sort descending
int product = accumulate(v.begin(), v.end(), 1, multiplies<int>());Other Useful Features
Regular Expressions (<regex>)
Regular expressions describe flexible text patterns — useful for validation, search, and substitution.
Pattern Syntax Reference
| Pattern | Matches |
|---|---|
. |
any single character |
\d / \D |
digit / non-digit |
\w / \W |
word char / non-word char |
\s / \S |
whitespace / non-whitespace |
a* |
zero or more a |
a+ |
one or more a |
a? |
zero or one a |
a{2,4} |
two to four a |
[abc] |
a, b, or c |
[^abc] |
anything except a, b, c |
[a-z] |
any lowercase letter |
^ / $ |
start / end of string |
\| |
alternation (or) |
(...) |
capturing group |
+and*match as many characters as possible (greedy). Use+?/*?for non-greedy matching.
Examples
regex r;
r = {"\\d*\\.?\\d+"}; // decimal number with optional decimal point
r = {"\\w+\\.txt"}; // text filename
r = {"[a-h][1-8]"}; // valid chess square
r = {"[A-Z][a-z]*"}; // word with uppercase first letter
r = {"\\d|\\."}; // digit or "."
r = {"^a"}; // "a" at the start
r = {"a$"}; // "a" at the endUsing regex_match, regex_search, regex_replace
#include <iostream>
#include <regex>
#include <string>
using namespace std;
int main() {
// regex_match: the whole string must match
regex email{R"(\w+@\w+\.\w+)"};
cout << regex_match("user@example.com", email) << '\n'; // 1
cout << regex_match("not-an-email", email) << '\n'; // 0
// regex_search: find a match anywhere in the string
string log = "Error 404: file not found";
smatch m;
if (regex_search(log, m, regex{R"(\d+)"}))
cout << "First number: " << m[0] << '\n'; // 404
// regex_replace: substitute all matches
string code = "hello world hello cpp hello";
cout << regex_replace(code, regex{"hello"}, "hi") << '\n';
// hi world hi cpp hi
}Iterating Over All Matches
string text = "alle meine entchen";
regex word{R"(\w+)"};
for (sregex_iterator it(text.begin(), text.end(), word);
it != sregex_iterator{}; ++it)
cout << (*it)[0] << '\n'; // alle / meine / entchenCapturing Groups
regex date{R"((\d{4})-(\d{2})-(\d{2}))"};
smatch match;
if (regex_search("Today is 2024-08-13.", match, date)) {
cout << "Year: " << match[1] << '\n'; // 2024
cout << "Month: " << match[2] << '\n'; // 08
cout << "Day: " << match[3] << '\n'; // 13
}Numeric Algorithms (<numeric>)
#include <numeric>
#include <vector>
#include <functional>
using namespace std;
int main() {
vector<int> v(7);
iota(v.begin(), v.end(), 1); // fill: 1,2,3,4,5,6,7
int sum = accumulate(v.begin(), v.end(), 0);
int prod = accumulate(v.begin(), v.end(), 1, multiplies<int>());
cout << "Sum: " << sum << '\n'; // 28
cout << "Product: " << prod << '\n'; // 5040
cout << "GCD: " << gcd(12, 18) << '\n'; // 6
cout << "LCM: " << lcm(12, 18) << '\n'; // 36
cout << "Mid: " << midpoint(3, 15) << '\n'; // 9
// prefix sums
vector<int> ps(v.size());
partial_sum(v.begin(), v.end(), ps.begin());
// ps = 1 3 6 10 15 21 28
}reduce works like accumulate but can evaluate out of order, enabling parallelism:
#include <numeric>
#include <execution>
// sequential, out-of-order accumulation
int sum = reduce(v.begin(), v.end(), 0);
// parallel (requires linking -ltbb on Linux)
sum = reduce(execution::par_unseq, v.begin(), v.end(), 0);Math Functions (<cmath>)
#include <cmath>
#include <cerrno>
#include <cstring>
using namespace std;
int main() {
double x = 2.5;
cout << abs(x) << '\n'; // 2.5
cout << floor(x) << '\n'; // 2
cout << ceil(x) << '\n'; // 3
cout << sqrt(x) << '\n'; // 1.58114
cout << pow(x,3) << '\n'; // 15.625
cout << log(x) << '\n'; // natural log
cout << sin(x) << '\n'; // also: cos, tan, sinh, ...
// error handling via errno
errno = 0;
double bad = sqrt(-1.0);
if (errno == EDOM) cerr << "Domain error: " << strerror(errno) << '\n';
errno = 0;
double huge = pow(2, 1e20);
if (errno == ERANGE) cerr << "Range error\n";
}Limits and Constants
<limits> provides type min/max values; <numbers> (C++20) provides mathematical constants:
#include <limits>
#include <numbers>
using namespace std;
cout << numeric_limits<int>::max() << '\n'; // 2147483647
cout << numeric_limits<int>::min() << '\n'; // -2147483648
cout << numeric_limits<double>::epsilon() << '\n'; // 2.22045e-16
using namespace std::numbers;
cout << pi << '\n'; // 3.14159...
cout << e << '\n'; // 2.71828...
cout << phi << '\n'; // 1.61803...
// templated for mixed precision
float area = pi_v<float> * r * r;Random Numbers (<random>)
The modern C++ approach separates the engine (generator) from the distribution, giving full control over randomness quality and statistical properties.
#include <iostream>
#include <random>
using namespace std;
int main() {
random_device rd; // hardware entropy source
mt19937 gen(rd()); // Mersenne Twister — high quality
uniform_int_distribution<int> dice(1, 6);
uniform_real_distribution<double> unit(0.0, 1.0);
normal_distribution<double> norm(0.0, 1.0);
for (int i = 0; i < 5; ++i) cout << dice(gen) << ' ';
cout << '\n' << unit(gen) << '\n' << norm(gen) << '\n';
// fixed seed for reproducible results (e.g. testing)
mt19937 fixed_gen(42);
cout << dice(fixed_gen) << '\n'; // always the same value
}Available distributions include: uniform_int_distribution, uniform_real_distribution, normal_distribution, binomial_distribution, poisson_distribution, exponential_distribution.
The old
rand() % napproach has unequal probabilities whenndoes not divideRAND_MAX. Prefer<random>for any serious use.
Time Measurement (<chrono>)
#include <iostream>
#include <chrono>
#include <vector>
#include <algorithm>
using namespace std;
using namespace std::chrono;
int main() {
vector<int> v(1'000'000);
// ... fill v ...
auto t1 = high_resolution_clock::now();
sort(v.begin(), v.end());
auto t2 = high_resolution_clock::now();
auto ms = duration_cast<milliseconds>(t2 - t1).count();
auto us = duration_cast<microseconds>(t2 - t1).count();
cout << "Sort took: " << ms << " ms (" << us << " µs)\n";
}Use high_resolution_clock for benchmarking. system_clock gives wall-clock time. C++20 adds full calendar and timezone support via std::chrono::year_month_day etc.
Complex Numbers (<complex>)
#include <complex>
#include <numbers>
using namespace std;
using namespace std::complex_literals;
complex<double> z0 = 1.0 + 2i;
complex<double> z1 = 3.0 - 1i;
cout << z0 + z1 << '\n'; // (4, 1)
cout << z0 * z1 << '\n'; // (5, 5)
cout << abs(z0) << '\n'; // magnitude
cout << arg(z0) << '\n'; // angle in radians
// Euler's formula: e^(i*pi) + 1 = 0
complex<double> euler = exp(1i * numbers::pi);
cout << euler << '\n'; // (-1, ~0)Vector Arithmetic (<valarray>)
vector<T> is a general-purpose container and does not support element-wise arithmetic. valarray is designed for numerical array operations:
#include <valarray>
using namespace std;
valarray<double> a = {1.0, 2.0, 3.0, 4.0};
valarray<double> b = {10.0, 20.0, 30.0, 40.0};
auto c = 2.0 * a + b; // element-wise: {12, 24, 36, 48}
auto d = sqrt(c); // element-wise sqrt
cout << c.sum() << '\n'; // 120
cout << c.min() << '\n'; // 12
cout << c.max() << '\n'; // 48