Reading Notes for Mastering the C++17 STL

This blog post is a quick chapter by chapter summary of the book Mastering the C++17 STL by Arthur O’Dwyer. The author makes difficult C++ concepts easy to understand and follow. The entire book is also a good summary of the newer C++17 STL offerings.
Chapter 1: Classical Polymorphism and Generic Programming
This chapter focuses on polymorphic functions vs generic templated functions, with a detailed example. However, the key takeaway from this chapter is that, C++ now has an extremely heavy paradigm shift towards generic templated programming. It is the key to understand quite a majority of the newer C++ features.
Chapter 2: Iterators and Ranges
- Iterators are a higher level abstractions than pointers. They provide a uniform interface to work with containers, and could be easier to work with for some (or most) containers. Containers and algorithms in STL have better support for iterators.
- A pair of iterators defines a (left closed, right open) range
- Iterator categories (companion article to read on cplusplus.com)
- InputIterator: can be incremented, dereferenced as rvalues, only use in single pass, cannot be meaningfully copied (think of istream)
- OutputIterator: can be incremented, dereferenced as lvalues, only use in single pass
- ForwardIterator: Built upon InputIterator, provides forward only traversal
- BidirectionalIterator: Built upon ForwardIterator, provides bidirectional traversal
- RandomAccessIterator: Built upon BidirectionalIterator, provides pointer-arithmetic like random access traversal
Chapter 3: The Iterator-Pair Algorithms
- Read-only range algorithms
distance(b, e): number of elements betweenbande(could be negative)count_if(a, b, p): number of elementsxthat satisfyp(x)betweenaandbcount(a, b, v): number of elementsxthat satisfyx==vbetweenaandb- Note:
std::countcould be outperformed by container specific functions (e.g.,std::set::count), same is true for some other algorithms find(a, b, v): returns iterator to the firstxsuch thatx==vfind_if(a, b, p): returns iterator to the firstxsuch thatp(x)find_if_not(a, b, p): returns iterator to the firstxsuch that!p(x)find_first_of(a1, b1, a2, b2): returns iterator to the firstxin[a1, b1)such thatxappears in[a2, b2)find_first_of(a1, b1, a2, b2, p): returns iterator to the firstxin[a1, b1)such thatxappears in[a2, b2)andp(x)holdsall_of(a, b, p): return true if for allxbetweenaandb,p(x)holdsany_of(a, b, p): return true if for somexbetweenaandb,p(x)holdsnone_of(a, b, p): return true if for noxbetweenaandb,p(x)holds
- Copy related algorithms
copy(InputIterator a, InputIterator b, OutputIterator c): copies range[a, b)into range starting atcmove(InputIterator a, InputIterator b, OutputIterator c): moves range[a, b)into range starting atc- Note: this
movealgorithm is defined in<algorithm>header, different from thestd::movedefined in<utility> transform(InputIterator a, InputIterator b, OutputIterator c, Unary op): copies allxin[a, b)asop(x)into range starting atc
- Write-only range algorithms
fill(a, b, v): fills elements in range[a, b)with valueviota(a, b, v): fills elements in range[a, b)with sequentially increasing values starting withvand repetitively evaluating++vgenerate(a, b, g): fills elements in range[a, b)with successive result ofg()
- Algorithms that affect object lifetime
destroy_at(T* p): explicitly calls destructor~T()onpaddressof(x): equivalent to&x(unlessxis of a class type that overloadsoperator&)uninitialized_copy(a, b, c): copies elements from the range[a, b)to an uninitialized memory area beginning atc
- Permutational algorithms:
sort(a, b): sort the range using default comparatorsort(a, b, cmp): sort the range using comparatorcmpstable_sort: guarantees the order of equal elements are of the same orderswap(a, b): swaps two elements. This is the most basic building block for classes to be used in a containerreverse(a, b): reverses the elements in the rangepartition(a, b, p): puts all elementsxso thatp(x)is true before all elementsywith!p(y). Not stable. There are alsois_partitionedandstable_partitionfunctions.rotate(a, m, b): rotates elements in[a, b)such thatmbecomes the first element and[a, m)have been rotated to after[m, b)next_permutation(a, b): reorders elements in[a, b)so that they are the next permutation of the existing elements (e.g., for astd::vector<int> x { 2, 1, 3 }, callingnext_permutation(x)changesxto{ 2, 3, 1 }). There is alsoprev_permutationto use.mismatch(a1, b1, a2, b2): returns the first mismatching pair of elements from two rangeslexicographical_compare(a1, b1, a2, b2): compares two ranges in lexicographical order
- Heaps, Heapsort, and Sorting
make_heap,pop_heap,push_heap,sort_heap: heap related algorithmsmerge,inplace_merge: merge algorithmslower_bound(a, b, v): returns iterator to the first element in the range[a, b)that’s not less thanv, orbif not foundupper_bound(a, b, v): returns iterator to the first element in the range[a, b)that’s greater thanv, orbif not foundremove_if(a, b, p): removes all elementsxin[a, b)satisfyingp(x), returns past-the-end iterator for the new end of range
Chapter 4: The Container Zoo
std::array<T, N>- Offers
begin()andend() - Overloads operators
=,==,< - Can return a
std::arrayfrom a function - Can put into containers
- Offers
std::vector<T>- Uses an array behind the scenes, can direct access via
data() - Grows by need automatically
- Can explicitly resize too, resizes new memory if new size is bigger
- Insert at the end via
push_backandemplace_back - Erase operation is linear complexity
vector<bool>has pitfalls: internally represented using bits to represent bools, efficient butoperator[]returnsstd::vector<bool>::referencetype, convertible toboolbut not directlybool.- If move constructors of a class not defined as
noexcept,std::vectoruses the copy constructor to do resizing, which is less efficient
- Uses an array behind the scenes, can direct access via
std::deque<T>- Roughly two ended vector that can grow on both directions, internally using arbitrary number of “chunks” of contiguous memory
- In addition to
std::vector<T>’s API functions, also haspush_frontandpop_front - No specialization of
std::deque<bool>
std::list<T>- Doubly linked list, dynamically allocating memories for elements
- Operations such as
splice,merge, and removing elements (remove,remove_if,unique) become cheap std::list::sortfunction is faster thanstd::sort- Can traverse bidirectionally
- No iterator invalidation
std::forward_list<T>- Mostly
std::list<T>, without some features - Can’t get size, can’t iterate backward
- Can’t do
splice,push_back - Provides some other remedy functions:
erase_after,insert_after,splice_after
- Mostly
std::stack<T, Ctr>,std::queue<T, Ctr>- An adapter, underlying storage managed by container type
Ctr - Should limit usage of the functions to the adapters only
- An adapter, underlying storage managed by container type
std::priority_queue<T, Ctr, Cmp>- Priority queue adapter
- Tree structures:
std::set<T>,std::map<K, V>- Elements are sorted in a binary search tree! Has impact on complexity of element lookup
std::multiset<T>,std::multimap<K, V>- Instead of a “counter” for the “multi” values, multiple elements are actually stored in the underlying BST
- Hash structures:
std::unordered_set<T>,std::unordered_map<K, V>- Based on hashes and buckets
- Can manipulate the underlying buckets and load factors
Chapter 5: Vocabulary Types
- A vocabulary type purports to provide a common language for dealing with its domain, e.g., numbers, strings, bools, size_t, time_t, …
std::reference_wrapper<T>: tag values we’d like to behave as referencesenum classstd::tuple: a fixed number of possibly different types of valuesstd::get<I>(t): returns reference to theIth element oftstd::tuple_size_v<decltype(t)>: size of the tupletstd::tuple_element_t<I, decltype(t)>: type of theIth element oftstd::tuple_cat(t1, t2, ...): concatenates all tuplest1,t2, … together as one tuplestd::forward_as_tuple(a, b, c, ...): creates a tuple of references that can be extracted bystd::get<I>(t)
std::pair: a tuple of two; hasfirstandsecondfieldsstd::variant<A, B, C, ...>: expressing an alternative type of eitherA,B, orC, …std::holds_alternative<double>(v): tells ifvholds adoublein itstd::get<double>(v): gets thedoublevalue out ofv- Use a “visitor” to visit different variant types
- There is no
make_variantas there aremake_tuple,make_pair
std::optional<T>- Using std::variant<std::monostate, int> could represent optionals with the
specific type
std::monostate(withstd::monostate{}as the value) - But using
std::optional<T>andstd::nulloptis more convenient has_value(): tells if the optional is in “engaged” state;o.has_value()is equal tobool(o), so you can put an optional directly inside if condition- overloads operators
==,!=,<,>,<=,>= o.value()gets the value if engaged, otherwise throws*oreturns a reference to the value contained ino, gets undefined behavior if it’s disengagedo.value_or(x)returns a copy of teh value contained byo, or a copy ofxif disengaged
- Using std::variant<std::monostate, int> could represent optionals with the
specific type
std::any: holding an element of any type- Implemented using type erasure
std::function: holding a function object
Chapter 6: Smart Pointers
std::unique_ptr<T>- Behaves just as
T* - Can’t be copied
- Once moved, ownership of underlying object transfers
- Can provide a deletion callback
- Supports
std::unique_ptr<T[]>(calling the correct delete function)
- Behaves just as
std::shared_ptr<T>- Shared ownership of objects, internally implements reference counter
- Contains two fields:
ptr(pointing to the managed object) andctrl(pointing to control block) - Control block:
vptr,use,weak,ptr(also pointing to the object),Deleter - Do not call
sp.get()and use that raw pointer to initialize anothershared_ptr!
Chapter 7: Concurrency
volatileandstd::atomic<T>serve different purposesvolatilefor memory that might change (between previous and next access)std::atomic<T>for thread safe access- Using atomic to do complex computation:
loadthe value first, then perform operation, then usecompare_exchange_weakto set the atomic value to the computed std::atomic<T>doesn’t work well with “big” types, such asstring. Need mutex for this purpose.
std::mutex- Basic usage: declare mutex as static inside a function scope, and call
lockandunlockto protect the operations - Problems with basic usage:
- locking a mutex and forget to unlock
- code has early return / exception so that mutex is not unlocked
- mutex are different variables than those they are protecting, so in some usages one forgets to take the mutex lock
- two or more mutex usage could cause deadlock
- More idiomatic, RAII style usage:
std::lock_guard - RUle of thumb: “always associate a mutex with its controlled data”
(including both read and write)
- On the surface, if a function is known to be only called from another which has the lock, then it is fine
- If a function can be called from a different thread, then must be lock protected
- Basic usage: declare mutex as static inside a function scope, and call
- Special purpose mutex types
std::recursive_mutex: remembers which thread has locked it; that thread can lock this mutex again, mutex counter is incremented; other threads will block until counter is 0std::timed_mutex: aware of passage of time; supportstry_lock_for()andtry_lock_until()that works with thechronolibrarystd::recursive_timed_mutex: combination of both- Read-write locks: use a
std::shared_timed_mutex; writers use astd::lock_guardon the mutex; readers use astd::shared_lockon the mutex - The current library does not allow “upgrading” a read lock
(
std::shared_lock) to a write lock (std::unique_lock) ordowngrading; must release lock and re-acquire
- Condition variables
std::condition_variablecv:cv.wait(lk)(lkis alock_guard) puts thread to sleep;cv.notify_one()wakes up one thread that’s currently waiting on cv;cv.notify_all()wakes up all
- Promises and futures – better treatment of waiting for conditions
std::promise<T> pcreates a variable that “promises” to give you aTin futurestd::future<T> f = p.get_future()obtains a handle to p’s futurep.set_value(42)is called in one threadf.get()has the value of the promisep- If
f.get()is called beforep.set_value(42)is called, then the calling thread is blocked and waits - There is a specialization for
std::promise<void>that’s just used for communicating “job done” and not pass value promiseandfuturedoesn’t get rid of mutex, locks, or condition variables, just hides them; there is this price to pay when using
- Packaged tasks: just like
std::functions but with afuture.
Chapter 8: Allocators
- An allocator is a handle to a memory resource, behaves analogously to
newanddelete - Supports two member functions:
allocate(i.e.,new) anddeallocate(i.e.,delete) - Supports one non-member function:
operator== - There are some other member / non-member functions but they are deprecated in C++17 and removed in C++20
- To create an allocator, implement the “interface”
std::pmr::memory_resource std::allocator<T>is a stateless empty type that provides a handle to the global heap management bynewanddelete- To make a
my_class<T>allocate-aware: addclass A = std::allocator<T>as template parameter; in theemplacefunction, obtain a pointer tostd::allocator_traits<A>; then usestatic_cast<T *>to turn that pointer into one formy_class<T>
Chapter 9: Iostreams
- Input stream stages: buffering, lexing, parsing
- Output stream stages: buffering, formatting
- Standard C APIs:
fopen,fclose,ftell,printf,snprintf, … - IO classes hierarchy (picture from this page)

- IO streams have a rich set of manipulators such as
set::setw(8),set::hexetc. ostringstreamhas been used as a string formatter; useostringstream::str()to obtain the final string- Converting numbers to strings
snprintf(buffer, sizeof buffer, "%d", int_value);oss << int_value; str = oss.str();str = std::to_string(int_value);r = std::to_chars(buffer, std::end(buffer), int_value, 10); *r.ptr = '\0'
- Converting strings to numbers
char* endptr; int_value = strtol(buffer, &endptr, 10);int rc = sscanf(buffer, "%d", &int_value);size_t endidx; int_value = std:stoi(str, &endidx, 10);iss.str("42"); iss >> int_value;std::from_chars_result r = std::from_chars(buffer, buffer + 2, int_value, 10);
- Floating numbers are similar, the
toiparts becometofin the functions - Reading a line of text:
std::string line; while (std::getline(std::cin, line)) { process(line); };
Chapter 10: Regular expressions
- Skimmed over this section; if needed, some online docs from cppreference.com could be good starting points
Chapter 11: Random numbers
- Skimmed over this section; if needed, some online docs from cppreference.com could be good starting points
Chapter 12: Filesystem
- Skimmed over this section; if needed, some online docs from cppreference.com could be good starting points
- One interesting quote from this section: In POSIX, file names are strings of bytes. In Windows, file names are strings of Unicode characters.