A data structure, such as a list or a vector, is not very useful on its own. To use one, we need operations for basic access — adding and removing elements — and, beyond that, we rarely just store objects in a container: we sort them, print them, extract subsets, remove elements, search for objects, and so on. Consequently, the standard library provides the most common algorithms for containers in addition to the most common container types. The thread running through this chapter: an algorithm is a function template operating on (half-open) sequences of elements, sequences are identified by pairs of iterators — not by containers — and iterators are the seam that separates data storage from computation.
Why it matters: once you think in sequences and iterators, one algorithm
serves every container, string, stream, and built-in array. "The standard-library algorithms
tend to be more carefully designed, specified, and implemented than the average hand-crafted
loop. Know them and use them in preference to code written in the bare language." The same
half-open [begin:end) convention you met in §1.7's range-for is what makes
sort(vec.begin(),vec.end()) and, in C++20, the equivalent
sort(vec) work.
The simplest example: sort a vector<Entry> and place a copy of each unique
element on a list:
void f(vector<Entry>& vec, list<Entry>& lst) {
sort(vec.begin(), vec.end()); // use < for order
unique_copy(vec.begin(), vec.end(), lst.begin()); // don't copy adjacent equal elements
}
bool operator<(const Entry& x, const Entry& y) { // less than
return x.name < y.name; // order Entries by their names
}
A standard algorithm is expressed in terms of (half-open) sequences: a
sequence is represented by a pair of iterators specifying the first element and the
one-beyond-the-last element. For writing (output), we need only to specify the first
element to be written; if more than one element is written, the elements following that
initial element will be overwritten. Thus, to avoid errors, lst must
have at least as many elements as there are unique values in vec. The standard
library doesn't offer an abstraction to support range-checked writing into a container — but
we can define one:
template<typename C> class Checked_iter {
public:
using value_type = typename C::value_type;
using difference_type = int;
Checked_iter() { throw Missing_container{}; } // concept forward_iterator requires a default constructor
Checked_iter(C& cc) : pc{&cc} {}
Checked_iter(C& cc, typename C::iterator pp) : pc{&cc}, p{pp} {}
Checked_iter& operator++() { check_end(); ++p; return *this; }
Checked_iter operator++(int) { check_end(); auto t{*this}; ++p; return t; }
value_type& operator*() const { check_end(); return *p; }
bool operator==(const Checked_iter& a) const { return p==a.p; }
bool operator!=(const Checked_iter& a) const { return p!=a.p; }
private:
void check_end() const { if (p == pc->end()) throw Overflow{}; }
C* pc {}; // default initialize to nullptr
typename C::iterator p {};
};
vector<int> v1 {1, 2, 3}; // three elements
vector<int> v2(2); // two elements
copy(v1, v2.begin()); // will overflow
copy(v1, Checked_iter{v2}); // will throw
If we had wanted to place the unique elements in a new list, we could use
back_inserter():
list<Entry> f(vector<Entry>& vec) {
list<Entry> res;
sort(vec.begin(), vec.end());
unique_copy(vec.begin(), vec.end(), back_inserter(res)); // append to res
return res;
}
The call back_inserter(res) constructs an iterator for res that adds
elements at the end of the container, extending the container to make room. This saves us from
first having to allocate a fixed amount of space and then filling it — the standard containers
plus back_inserter()s eliminate the need to use error-prone, explicit C-style
memory management using realloc(). The standard-library list has a move
constructor (§6.2.2) that makes returning res by value efficient (even for lists
of thousands of elements).
When the pair-of-iterators style feels tedious, use the range versions:
sort(vec) is equivalent to sort(vec.begin(),vec.end()) (§13.5).
Similarly, a range-for loop is roughly equivalent to a C-style loop using iterators directly —
and in addition to being simpler and less error-prone, the range-for version is often also
more efficient.
For a container, a few iterators referring to useful elements can be obtained;
begin() and end() are the best examples. In addition,
many algorithms return iterators. For example, find looks for a
value in a sequence and returns an iterator to the element found:
bool has_c(const string& s, char c) { // does s contain the character c?
auto p = find(s.begin(), s.end(), c);
if (p != s.end()) return true;
else return false;
}
// Like many standard-library search algorithms, find returns end() to indicate "not found".
bool has_c(const string& s, char c) { return find(s, c) != s.end(); } // range version
A more interesting exercise: find the location of all
occurrences of a character in a string, returned as a
vector<string::iterator>. Returning a vector is efficient because vector
provides move semantics (§6.2.1):
vector<string::iterator> find_all(string& s, char c) { // find all occurrences of c in s
vector<string::iterator> res;
for (auto p = s.begin(); p != s.end(); ++p)
if (*p == c) res.push_back(p);
return res;
}
void test() {
string m {"Mary had a little lamb"};
for (auto p : find_all(m, 'a'))
if (*p != 'a') cerr << "a bug!\n";
}
Iterators and standard algorithms work equivalently on every standard container for which their use makes sense, so we can generalize:
template<typename C, typename V> // find all occurrences of v in c
vector<typename C::iterator> find_all(C& c, V v) {
vector<typename C::iterator> res;
for (auto p = c.begin(); p != c.end(); ++p)
if (*p == v) res.push_back(p);
return res;
}
The typename is needed to inform the compiler that C::iterator is
supposed to be a type and not a value of some type, say, the integer 7.
Alternatively, we could have returned a vector of ordinary pointers to the elements, using a
range-for and the standard-library range_value_t (§16.4.4) to name the element
type — with a simplified
template<typename T> using range_value_type_t = T::value_type;. The same
find_all() then works for a string, a list<int>,
and a vector<string>, and we can even modify found elements through the
returned iterators (*p = "vert";).
Iterators are used to separate algorithms and containers.
An algorithm operates on its data through iterators and knows nothing about the container in
which the elements are stored. Conversely, a container knows nothing about the algorithms
operating on its elements; all it does is supply iterators upon request (e.g.,
begin() and end()). This model of separation between data storage
and algorithm delivers very general and flexible software.
What are iterators really? Any particular iterator is an object of some type, and there are many different iterator types — an iterator needs to hold the information necessary for doing its job for a particular container type. These types can be as different as the containers and the specialized needs they serve:
vector's iterator could be an ordinary pointer, because a
pointer is quite a reasonable way of referring to an element of a vector.
list iterator must be something more complicated than a simple pointer to an
element, because an element of a list in general does not know where the next element is.
Thus, a list iterator might be a pointer to a link.
What is common for all iterators is their semantics and the naming of their
operations: applying ++ to any iterator yields an iterator that refers to the
next element; applying * yields the element to which the iterator refers. In
fact, any object that obeys a few simple rules like these is an iterator.
Iterator is a general idea, a concept (§8.2); different kinds of iterators
are made available as standard-library concepts, such as forward_iterator and
random_access_iterator (§14.5). Furthermore, users rarely need to know the type
of a specific iterator; each container "knows" its iterator types and makes them available
under the conventional names iterator and const_iterator. In some
cases an iterator is not a member type, so the standard library offers
iterator_t<X> that works wherever X's iterator is defined.
Containers are not the only place where we find sequences of elements. An input stream produces a sequence of values, and we write a sequence of values to an output stream — so the notion of iterators can be usefully applied to input and output.
ostream_iterator<string> oo {cout}; // write strings to cout
*oo = "Hello, "; // meaning cout << "Hello, "
++oo;
*oo = "world!\n"; // meaning cout << "world!\n"
The effect of assigning to *oo is to write the assigned value to
cout; the ++oo mimics writing into an array through a pointer. That
way, we can use algorithms on streams: copy(v, oo) writes all of
v to cout.
Similarly, an istream_iterator treats an input stream as a read-only container —
we must specify the stream and the type of values expected (istream_iterator<string> ii {cin};). Input iterators are used in pairs, so we must provide an istream_iterator to
indicate the end of input — this is the default
istream_iterator<string> eos {};. Typically they are not used directly but
provided as arguments to algorithms. The classic file-word-sort program:
int main() {
string from, to;
cin >> from >> to; // get source and target file names
ifstream is {from}; // input stream for file "from"
istream_iterator<string> ii {is}; // input iterator for stream
istream_iterator<string> eos {}; // input sentinel
ofstream os {to}; // output stream for file "to"
ostream_iterator<string> oo {os, "\n"}; // output iterator plus a separator
vector<string> b {ii, eos}; // b is a vector initialized from input
sort(b); // sort the buffer
unique_copy(b, oo); // copy the buffer to output, discard replicated values
return !is.eof() || !os; // return error state (§1.2.1, §11.4)
}
I used the range versions of sort() and unique_copy(); I could have
used iterators directly, e.g. sort(b.begin(),b.end()), as is common in older
code. Remember that to use both a traditional iterator version of a standard-library algorithm
and its ranges counterpart, we need to either explicitly qualify the call of the
range version or use a using-declaration (§9.3.2):
copy(v, oo); // potentially ambiguous
ranges::copy(v, oo); // OK
using ranges::copy; // copy(v, oo) OK from here on
copy(v, oo); // OK
An ifstream is an istream attached to a file (§11.7.2); an
ofstream is an ostream attached to a file; the
ostream_iterator's second argument is used to delimit output values. The program
is longer than it needs to be — a more elegant solution doesn't store duplicates at all, by
keeping the strings in a set, which doesn't keep duplicates and keeps its
elements in order (§12.5). Then two lines using a vector collapse into one using a set, and
unique_copy() is replaced by the simpler copy():
int main() {
string from, to;
cin >> from >> to;
ifstream is {from};
ofstream os {to};
set<string> b {istream_iterator<string>{is}, istream_iterator<string>{}}; // read input
copy(b, ostream_iterator<string>{os, "\n"}); // copy to output
return !is.eof() || !os;
}
In the examples so far, the algorithms have simply "built in" the action to be done for each
element of a sequence. However, we often want to make that action a parameter to the
algorithm. For example, find provides a convenient way of looking for a specific
value; a more general variant, find_if, looks for an element that fulfills a
specified requirement — a predicate. For instance, we might want to search a
map for the first value larger than 42. A map allows us to access its elements as
a sequence of (key,value) pairs, so we search a map<string,int>'s sequence
for a pair<const string,int> where the int is greater than 42.
Greater_than is a function object (§7.3.2) holding the value (42) to be compared
against a map entry:
struct Greater_than {
int val;
Greater_than(int v) : val{v} {}
bool operator()(const pair<string,int>& r) const { return r.second > val; }
};
auto p = find_if(m, Greater_than{42}); // m is a map<string,int>
// Alternatively and equivalently, a lambda expression (§7.3.2):
auto p = find_if(m, [](const auto& r) { return r.second > 42; });
A predicate should not modify the elements to which it is applied.
A general definition of an algorithm: "a finite set of rules which gives a sequence of
operations for solving a specific set of problems [and] has five important features:
Finiteness, Definiteness, Input, Output, Effectiveness" [Knuth, 1968, §1.1]. In the context of
the C++ standard library,
an algorithm is a function template operating on sequences of elements. The
standard library provides many dozens of algorithms, defined in namespace std and
presented in the <algorithm> and <numeric> headers.
These all take sequences as inputs; a half-open sequence from b to
e is referred to as [b:e). A few examples:
| Algorithm | Meaning |
|---|---|
f = for_each(b,e,f) |
For each element x in [b:e) do f(x) |
p = find(b,e,x) |
p is the first p in [b:e) so that *p==x |
p = find_if(b,e,f) |
p is the first p in [b:e) so that f(*p) |
n = count(b,e,x) |
n is the number of elements *q in [b:e) so that *q==x |
n = count_if(b,e,f) |
n is the number of elements *q in [b:e) so that f(*q) |
replace(b,e,v,v2) |
Replace elements *q in [b:e) so that *q==v with v2 |
replace_if(b,e,f,v2) |
Replace elements *q in [b:e) so that f(*q) with v2 |
p = copy(b,e,out) |
Copy [b:e) to [out:p) |
p = copy_if(b,e,out,f) |
Copy elements *q from [b:e) so that f(*q) to [out:p) |
p = move(b,e,out) |
Move [b:e) to [out:p) |
p = unique_copy(b,e,out) |
Copy [b:e) to [out:p), not copying adjacent equal elements
|
sort(b,e) / sort(b,e,f) |
Sort [b:e) using < / f as the ordering criterion |
(p1,p2) = equal_range(b,e,v) |
p1 is the first p such that v<*p; p2 is the first p such that *p<v |
p = merge(b,e,b2,e2,out) |
Merge two sorted sequences [b:e) and [b2:e2) into
[out:p)
|
For each algorithm taking a [b:e) range, the <ranges> library
offers a version that takes a range. These algorithms — and many more (e.g., §17.3) — can be
applied to elements of containers, strings, and built-in arrays. Two structural facts to
remember:
replace() and sort(), modify element
values, but
no algorithm adds or subtracts elements of a container. The reason: a
sequence does not identify the container that holds the elements of the sequence. To add or
delete elements, you need something that knows about the container — e.g., a
back_inserter
(§13.1) — or directly refers to the container itself (e.g.,
push_back() or erase(), §12.2).
for_each(v, [](int& x){ x = x*x; }); to square every element, or
for_each(v.begin(), v.begin()+3, [](int& x){ x = sqrt(x); }); to transform
just the first three.
The standard-library algorithms tend to be more carefully designed, specified, and implemented than the average hand-crafted loop. Know them and use them in preference to code written in the bare language.
When the same task is to be done to many data items, we can execute it in parallel on each data item, provided the computations on different data items are independent:
The standard library offers support for both, and we can be specific about wanting sequential
execution. In <execution>, in namespace execution, we find:
seq (sequential execution), par (parallel execution, if feasible),
unseq (unsequenced/vectorized execution, if feasible), and
par_unseq (parallel and/or unsequenced vectorized execution, if feasible).
Consider std::sort():
sort(v.begin(), v.end()); // sequential
sort(seq, v.begin(), v.end()); // sequential (same as the default)
sort(par, v.begin(), v.end()); // parallel
sort(par_unseq, v.begin(), v.end()); // parallel and/or vectorized
Whether it is worthwhile to parallelize and/or vectorize depends on the algorithm, the number of elements in the sequence, the hardware, and the utilization of that hardware by programs running on it. Consequently, the execution policy indicators are just hints: a compiler and/or run-time scheduler will decide how much concurrency to use. This is all nontrivial, and the rule against making statements about efficiency without measurement is very important here. Unfortunately, the range versions of the parallel algorithms are not yet in the standard — but if we need them, they are easy to define:
void sort(auto pol, random_access_range auto& r) { sort(pol, r.begin(), r.end()); }
Most standard-library algorithms, including all in the §13.5 table except
equal_range, can be requested to be parallelized and vectorized using
par and par_unseq as for sort(). Why not
equal_range()? Because so far nobody has come up with a worthwhile parallel
algorithm for that. Many parallel algorithms are used primarily for numeric data (see
§17.3.1). When requesting parallel execution, be sure to avoid data races (§18.2) and deadlock
(§18.3).
Here is a summary of the guidance from this chapter. All 11 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.
| # | Guideline | § |
|---|---|---|
| 1 | An STL algorithm operates on one or more sequences. | 13.1 |
| 2 | An input sequence is half-open and defined by a pair of iterators. | 13.1 |
| 3 | You can define your own iterators to serve special needs. | 13.1 |
| 4 | Many algorithms can be applied to I/O streams. | 13.3.1 |
| 5 | When searching, an algorithm usually returns the end of the input sequence to indicate "not found". | 13.2 |
| 6 | Algorithms do not directly add or subtract elements from their argument sequences. | 13.2, 13.5 |
| 7 | When writing a loop, consider whether it could be expressed as a general algorithm. | 13.2 |
| 8 | Use using-type-aliases to clean up messy notation. |
13.2 |
| 9 | Use predicates and other function objects to give standard algorithms a wider range of meanings. | 13.4, 13.5 |
| 10 | A predicate must not modify its argument. | 13.4 |
| 11 | Know your standard-library algorithms and prefer them to hand-crafted loops. | 13.5 |
What exactly does a standard-library algorithm operate on, and why must an output sequence have room for what is written?
How does back_inserter(res) fix the overflow problem of writing into a
container, and what does it replace?
How do standard search algorithms like find report "not found", and what
makes the one-line has_c valid?
Why does the generic find_all need typename before
C::iterator, and why is returning the vector of iterators cheap?
How do iterators separate algorithms from containers, and what does each side know about the other?
Why can a vector's iterator be a plain pointer while a list's cannot, and what is common across all iterator types?
How do istream_iterator and ostream_iterator let algorithms work
on streams, and what is the input sentinel?
Why can copy(v, oo) be ambiguous, and how do you resolve it?
What does a predicate let find_if do that find cannot, and what
rule must a predicate obey?
Why can no standard-library algorithm add or remove elements, and where do the algorithms live?
What do seq, par, unseq, and
par_unseq promise, and why is equal_range excluded?
Why does the set-based version of the file-word-sort program need only
copy() instead of sort() + unique_copy()?
algorithm ⇒ function template on sequences →
[b:e) half-open pair of iterators
output ⇒ only first element given → values overwrite following elements → dest must have room
back_inserter(res) ⇒ appends + extends → kills
realloc() style management
range versions ⇒ sort(vec) ≡
sort(vec.begin(),vec.end()) ; move ctor → cheap by-value return
find returns end() ⇒ universal "not found"
→ compare p != s.end()
find_all ⇒ return vector<C::iterator> |
typename required (type not value) | move semantics
→ cheap return
range_value_t<C> ⇒ name the element type ;
using range_value_type_t = T::value_type;
separation ⇒ algorithm knows nothing about container | container
only supplies begin()/end()
vector ⇒ plain pointer (or pointer+index → range checking) ; list ⇒ pointer to link
common ⇒ ++ next element | * element →
concept (forward_iterator, random_access_iterator)
names ⇒ iterator / const_iterator ; non-member →
iterator_t<X>
ostream_iterator<T> oo {cout} ⇒ *oo = v writes
| ++oo mimics pointer ; 2nd arg ⇒ separator
istream_iterator ⇒ read-only stream-as-sequence | default
ctor ⇒ end-of-input sentinel (eos{})
word-sort program ⇒ vector{ii,eos} → sort →
unique_copy ; set version ⇒ dedupe + order at insert
→ just copy(b,oo)
ambiguity ⇒ copy(v,oo) potentially ambiguous →
ranges::copy(v,oo) or using ranges::copy;
find ⇒ specific value v/s find_if
⇒ requirement (predicate)
function object ⇒ Greater_than{42} holds state | lambda
⇒ equivalent [](const auto& r){ return r.second>42; }
rule ⇒ predicate must not modify elements
headers ⇒ <algorithm> + <numeric>
| range versions ⇒ <ranges>
table ⇒ for_each, find(_if), count(_if), replace(_if), copy(_if), move, unique_copy, sort, equal_range, merge
values yes, elements no ⇒ no algorithm adds/removes elements
→ sequence ≠ container → need back_inserter /
push_back / erase
lambdas ⇒ common as operations (for_each(v, [](int& x){ x = x*x; }))
rule 11 ⇒ prefer standard algorithms to hand-crafted loops
<execution> ⇒ seq | par
(threads) | unseq (SIMD) |
par_unseq (both) — hints
sort(par, b, e) ; measure before claiming speedup
equal_range ⇒ no worthwhile parallel version yet ; range+parallel
versions ⇒ not yet standard
hazards ⇒ data races §18.2 | deadlock §18.3 ; numeric use ⇒ §17.3.1
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 13: "Algorithms." Addison-Wesley.
Reference:
Chapter 1–12 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
algorithms library,
iterators library, and
execution policies.
Questions? Ask your agent — your teacher — about anything unclear: why end() is
the "not found" convention, when a plain pointer can be a vector iterator, or whether a loop
you just wrote could be a standard algorithm. Follow-ups are expected, not optional.