Lesson 16: Utilities

Lesson 0016 — A Tour of C++, Chapter 16 (§16.1–§16.9)

Labeling a library component as a "Utility" isn't very informative — every library component has been of utility to someone, somewhere, at some point in time. The facilities presented here are chosen because they serve critical purposes for many but their description doesn't fit elsewhere: time (<chrono>), function adaption (lambdas, mem_fn(), std::function), type functions (compile-time computation on types), source locations, move()/forward(), bit manipulation, and exiting a program. Often they act as building blocks for more powerful library facilities, including other components of the standard library. The thread running through this chapter: the library knows — about time zones, type properties, and clean exits — so we don't have to handcraft these subtleties.

Why it matters: these small components are the foundations the rest of the library — and your programs — stand on. The _v/_t type functions let generic code branch on type properties at compile time (the machinery behind concepts from Chapter 8); std::move() and std::forward() are the explicit levers for the move semantics of Chapter 6; source_location puts caller-context into diagnostics; and knowing exactly which exit function is right — and that none of them run destructors — is part of writing code that ends programs honestly.

16.1 Introduction

A library doesn't have to be large or complicated to be useful. The facilities in this chapter serve critical purposes for many programmers but their description doesn't fit elsewhere; they are the building blocks for more powerful library facilities.

16.2 Time

In <chrono>, the standard library provides facilities for dealing with time:

16.2.1 Clocks

Here is the basic way of timing some action:

using namespace std::chrono;              // sub-namespace std::chrono (§3.3)
auto t0 = system_clock::now();
do_work();
auto t1 = system_clock::now();
cout << t1-t0 << "\n";                    // default unit: 20223[1/10000000]s
cout << duration_cast<milliseconds>(t1-t0).count() << "ms\n";   // 2ms
cout << duration_cast<nanoseconds>(t1-t0).count() << "ns\n";    // 2022300ns

The clock returns a time_point (a point in time); subtracting two time_points gives a duration (a period of time). The default << for a duration adds an indication of the unit as a suffix. Various clocks give their results in various units of time ("clock ticks" — the clock shown measures in hundreds of nanoseconds), so it is often a good idea to convert a duration into an appropriate unit — that's what duration_cast does.

The clocks are useful for quick measurements. Don't make statements about "efficiency" of code without first doing time measurements — guesses about performance are most unreliable. Quick, simple measurements are better than none, but performance of modern computers is a tricky topic: always measure repeatedly to lower the chance of getting blindsided by rare events or cache effects. Namespace std::chrono_literals defines time-unit suffixes (§6.6):

this_thread::sleep_for(10ms + 33us);   // wait for 10 milliseconds and 33 microseconds

16.2.2 Calendars

When dealing with everyday events, we rarely use milliseconds; we use years, months, days, hours, seconds, and days of the week. The standard library supports that:

auto spring_day = April/7/2018;
cout << weekday(spring_day) << '\n';                // Sat
cout << format("{:%A}\n", weekday(spring_day));     // Saturday
// (For obscure reasons, %A means "write the day of the week's full name".)

auto spring_day2 = 2018y/April/7;    // the y suffix distinguishes years from plain ints
// (plain ints are used for days of the month, numbered 1 to 31)

It is possible to express invalid dates — if in doubt, check with ok():

auto bad_day = January/0/2024;
if (!bad_day.ok()) cout << bad_day << " is not a valid day\n";

Dates are composed by overloading operator / by the types year, month, and int. The resulting Year_month_day type has conversions to and from time_point for accurate and efficient computation involving dates:

sys_days t = sys_days{February/25/2022};   // a time point with the precision of days
t += days{7};                               // one week after February 25, 2022
auto d = year_month_day(t);                 // convert the time point back to the calendar
cout << d << '\n';                          // 2022-03-04 (ISO 8601 standard format)
cout << format("{:%B}/{}/{}\n", d.month(), d.day(), d.year());   // March/04/2022
// (For obscure reasons, %B means "write the month's full name".)

This calculation requires a change of month and knowledge about leap years. Such operations can often be done at compile time and are therefore surprisingly fast:

static_assert(weekday(April/7/2018) == Saturday);   // true

Calendars are complex and subtle — typical of and appropriate for "systems" designed for "ordinary people" over centuries, rather than by programmers to simplify programming. The standard-library calendar system can be (and has been) extended to cope with Julian, Islamic, Thai, and other calendars.

16.2.3 Time Zones

One of the trickiest issues to get right is time zones: they are so arbitrary that they are hard to remember, and they change in a variety of ways at times that are not standardized across the globe:

auto tp = system_clock::now();     // tp is a time_point
cout << tp << '\n';                // 2021-11-27 21:36:08.2085095
zoned_time ztp { current_zone(), tp };
cout << ztp << '\n';               // 2021-11-27 16:36:08.2085095 EST
const time_zone est {"Europe/Copenhagen"};
cout << zoned_time{ &est, tp } << '\n';   // 2021-11-27 22:36:08.2085095 GMT+1

A time_zone is a time relative to a standard (called GMT or UTC) used by the system_clock. The standard library synchronizes with a global database (IANA) to get its answers right; that synchronization can be automatic in the operating system or under the control of a system administrator. The names of time zones are C-style strings of the form "continent / major city", such as "America/New_York", "Asia/Tokyo", "Africa/Nairobi". A zoned_time is a time_zone together with a time_point.

Like calendars, time zones address a set of concerns we should leave to the standard library rather than rely on our own handcrafted code — "At what time of day on the last day of February 2024 in New York will the date change in New Delhi?" The standard library "knows."

16.3 Function Adaption

When passing a function as a function argument, the type of the argument must exactly match the expectations in the called function's declaration. If an intended argument only "almost matches expectations," we have three alternative ways of adjusting it:

16.3.1 Lambdas as Adaptors

The classical "draw all shapes" example:

void draw_all(vector<Shape*>& v) {
    for_each(v.begin(), v.end(), [](Shape* p) { p->draw(); });
}

Like all standard-library algorithms, for_each() calls its argument using the traditional function call syntax f(x), but Shape::draw() uses the conventional object-oriented notation x->f(). A lambda easily mediates between the two notations.

16.3.2 mem_fn()

Given a member function, the function adaptor mem_fn(mf) produces a function object that can be called as a nonmember function:

void draw_all(vector<Shape*>& v) {
    for_each(v.begin(), v.end(), mem_fn(&Shape::draw));
}

Before the introduction of lambdas in C++11, mem_fn() and equivalents were the main way to map from the object-oriented calling style to the functional one.

16.3.3 function

The standard-library function is a type that can hold any object you can invoke using the call operator () — an object of type function is a function object (§7.3.2):

int f1(double);
function<int(double)> fct1 {f1};            // initialize to f1
int f2(string);
function fct2 {f2};                          // fct2's type is function<int(string)> (deduced)
function fct3 = [](Shape* p) { p->draw(); }; // fct3's type is function<void(Shape*)>

Functions are useful for callbacks, for passing operations as arguments, for passing function objects, etc. However, they may introduce some run-time overhead compared to direct calls: for a function object whose size is not computed at compile time, a free-store allocation might occur, with seriously bad implications for performance-critical applications (a solution is coming for C++23: move_only_function). Another problem: function, being an object, does not participate in overloading — if you need to overload function objects (including lambdas), consider overloaded (§15.4.1).

16.4 Type Functions

A type function is a function evaluated at compile time, taking a type as its argument or returning a type. The standard library provides a variety of them to help library implementers (and programmers in general) write code that takes advantage of aspects of the language and the standard library. For numerical types, numeric_limits from <limits> presents useful information (§17.7); object sizes come from the built-in sizeof operator (§1.4); and in <type_traits> the library provides many functions for inquiring about properties of types:

bool b = is_arithmetic_v<X>;      // true if X is one of the (built-in) arithmetic types
using Res = invoke_result_t<decltype(f)>;   // Res is int if f returns an int

Some type functions create new types based on inputs:

template<typename T> using Store = conditional_t<sizeof(T) < max, On_stack<T>, On_heap<T>>;
// If the first (Boolean) argument to conditional_t is true, the result is
// the first alternative; otherwise, the second. Assuming On_stack and On_heap
// offer the same access functions to T, Store<X> users are tuned by the size of X.

Concepts are type functions — when used in expressions, they are specifically type predicates. In many cases concepts are the best type functions, but most of the standard library was written pre-concepts and must support pre-concept code bases. The notational conventions are confusing: the standard library uses _v for type functions that return values and _t for type functions that return types — a leftover from the weakly typed days of C and pre-concept C++. No standard-library type function returns both a type and a value, so these suffixes are redundant; with concepts, no suffix is needed or used. Type functions are part of C++'s mechanisms for compile-time computation, allowing tighter type checking and better performance — this use is often called metaprogramming (or, with templates, template metaprogramming).

16.4.1 Type Predicates

In <type_traits>, the standard library offers dozens of simple type functions called type predicates that answer fundamental questions about types (all return a bool):

Predicate Meaning
is_void_v<T> Is T void?
is_integral_v<T> Is T an integral type?
is_floating_point_v<T> Is T a floating-point type?
is_class_v<T> Is T a class (and not a union)?
is_function_v<T> Is T a function (and not a function object or a pointer to function)?
is_arithmetic_v<T> Is T an integral or floating-point type?
is_scalar_v<T> Is T an arithmetic, enumeration, pointer, or pointer-to-member type?
is_constructible_v<T, A...> Can a T be constructed from the A... argument list?
is_default_constructible_v<T> Can a T be constructed without explicit arguments?
is_copy_constructible_v<T> Can a T be constructed from another T?
is_move_constructible_v<T> Can a T be moved or copied into another T?
is_assignable_v<T,U> Can a U be assigned to a T?
is_trivially_copyable_v<T,U> Can a U be assigned to a T without user-defined copy operations?
is_same_v<T,U> Is T the same type as U?
is_base_of_v<T,U> Is U derived from T, or is U the same type as T?
is_convertible_v<T,U> Can a T be implicitly converted to a U?
is_iterator_v<T> Is T an iterator type?
is_invocable_v<T, A...> Can a T be called with the argument list A...?
has_virtual_destructor_v<T> Does T have a virtual destructor?

One traditional use is to constrain template arguments — e.g., a complex<Scalar> declared with static_assert(is_arithmetic_v<Scalar>, "Sorry, I support only complex of arithmetic types");. That — like other traditional uses — is easier and more elegantly done using concepts: template<Arithmetic Scalar> class complex. In many cases, type predicates disappear into the definition of concepts: concept Arithmetic = is_arithmetic_v<T>; (curiously, there is no std::arithmetic concept).

Most often, uses of the standard-library type predicates are found deep in the implementation of fundamental services, often to distinguish cases for optimization — e.g., part of the implementation of std::copy could optimize contiguous sequences of simple types: if constexpr (is_trivially_copyable_v<T>) memcpy(...); else .... That simple optimization beat its non-optimized variant by about 50% on some implementations. But: do not indulge in such cleverness unless you have verified that the standard doesn't already do better — hand-optimized code is typically less maintainable than simpler alternatives.

16.4.2 Conditional Properties

Consider defining a "smart pointer": operator->() should be defined if and only if T is a class type — Smart_pointer<vector<T>> should have ->, but Smart_pointer<int> should not. We cannot use a compile-time if because we are not inside a function. Instead:

template<typename T> class Smart_pointer {
    // ...
    T& operator*() const;
    T* operator->() const requires is_class_v<T>;   // -> is defined iff T is a class
};
// Or with a concept (there is no standard-library class-type concept):
template<typename T> concept Class = is_class_v<T> || is_union_v<T>;   // unions are classes
template<typename T> class Smart_pointer {
    // ...
    T* operator->() const requires Class<T>;   // -> is defined iff T is a class or a union
};

Often, a concept is more general or simply more appropriate than the direct use of a standard-library type predicate.

16.4.3 Type Generators

Many type functions return types, often new types that they compute — type generators. The standard offers a few:

Generator Meaning
R = remove_const_t<T> R is T with the topmost const (if any) removed
R = add_const_t<T> R is const T
R = remove_reference_t<T> If T is a reference U&, R is U; otherwise T
R = add_lvalue_reference_t<T> If T is an lvalue reference, R is T; otherwise T&
R = add_rvalue_reference_t<T> If T is an rvalue reference, R is T; otherwise T&&
R = enable_if_t<b, T=void> If b is true, R is T; otherwise R is not defined
R = conditional_t<b,T,U> R is T if b is true; U otherwise
R = common_type_t<T...> If there is a type that all Ts can be implicitly converted to, R is that type; otherwise R is not defined
R = underlying_type_t<T> If T is an enumeration, R is its underlying type; otherwise error
R = invoke_result_t<T,A...> If a T can be called with arguments A..., R is its result type

These are typically used in the implementation of utilities rather than directly in application code. Of these, enable_if is probably the most common in pre-concepts code — the conditionally enabled -> is traditionally implemented as enable_if<is_class_v<T>, T*> operator->(); — but it isn't particularly easy to read, and more complicated uses are far worse. The definition of enable_if relies on a subtle language feature called SFINAE ("Substitution Failure Is Not An Error") — look that up (only) if you need to.

16.4.4 Associated Types

All standard containers and all containers designed to follow their pattern have associated types such as their value types and iterator types. In <iterator> and <ranges>, the standard library supplies names for those:

Name Meaning
range_value_t<R> The type of the range R's elements
iter_value_t<T> The type of elements pointed to by the iterator T
iterator_t<R> The type of the range R's iterator

16.5 source_location

When writing out a trace message or an error message, we often want a source location to be part of that message. The library provides source_location for that:

const source_location loc = source_location::current();
// current() returns a source_location describing the spot in the source code where it appears.
// Class source_location has file() and function_name() members returning C-style strings,
// and line() and column() members returning unsigned integers.

void log(const string& mess = "", const source_location loc = source_location::current()) {
    cout << loc.file_name() << '(' << loc.line() << ':' << loc.column() << ") "
         << loc.function_name() << ": " << mess;
}

void foo() { log("Hello"); }   // myfile.cpp (17,4) foo: Hello

The call of current() is a default argument so that we get the location of the caller of log() rather than the location of log() itself. Code written before C++20 — or needing to compile on older compilers — uses the macros __FILE__ and __LINE__ for this.

16.6 move() and forward()

The choice between moving and copying is mostly implicit (§3.4): a compiler prefers to move when an object is about to be destroyed (as in a return). Sometimes we must be explicit — a unique_ptr is the sole owner of an object and cannot be copied, so to get it elsewhere you must move it:

void f1() {
    auto p = make_unique<int>(2);
    auto q = p;        // error: we can't copy a unique_ptr
    auto q = move(p);  // OK: p now holds nullptr
}

Confusingly, std::move() doesn't move anything — it casts its argument to an rvalue reference, thereby saying the argument will not be used again and therefore may be moved (§6.2.2). It should have been called something like rvalue_cast. It exists to serve a few essential cases, e.g. swap:

template <typename T> void swap(T& a, T& b) {
    T tmp {move(a)};   // the T constructor sees an rvalue and moves
    a = move(b);       // the T assignment sees an rvalue and moves
    b = move(tmp);     // the T assignment sees an rvalue and moves
}

As with other casts, there are tempting but dangerous uses of std::move():

string s1 = "Hello";
string s2 = "World";
vector<string> v;
v.push_back(s1);            // use a "const string&" argument; push_back() will COPY
v.push_back(move(s2));      // use a move constructor — sometimes only sometimes cheaper
// The problem: a moved-from object is left behind:
cout << s1[2];              // 'l'
cout << s2[2];              // crash?

Stroustrup considers this use of std::move() too error-prone for widespread use — don't use it unless you can demonstrate significant and necessary performance improvement, because later maintenance may accidentally lead to unanticipated use of the moved-from object. The compiler knows a return value is not used again, so return std::move(x) is redundant and can even inhibit optimizations. The state of a moved-from object is in general unspecified, but all standard-library types leave a moved-from object in a state where it can be destroyed and assigned to; for a container (vector, string), the moved-from state will be "empty."

Forwarding arguments is an important use case that requires moves (§8.4.2): we sometimes want to transmit a set of arguments on to another function without changing anything (perfect forwarding):

template<typename T, typename... Args> unique_ptr<T> make_unique(Args&&... args) {
    return unique_ptr<T>{new T{std::forward<Args>(args)...}};   // forward each argument
}

The standard-library forward() differs from the simpler std::move() by correctly handling subtleties to do with lvalue and rvalue (§6.2.2). Use std::forward() exclusively for forwarding, and don't forward() something twice — once you have forwarded an object, it's not yours to use anymore.

16.7 Bit Manipulation

In <bit>, we find functions for low-level bit manipulation — a specialized but often essential activity. When we get close to the hardware, we often have to look at bits, change bit patterns in a byte or a word, and turn raw memory into typed objects. For example, bit_cast lets us convert a value of one type to another type of the same size:

double val = 7.2;
auto x = bit_cast<uint64_t>(val);    // get the bit representation of a 64-bit floating point number
auto y = bit_cast<uint64_t>(&val);   // get the bit representation of a 64-bit pointer

struct Word { std::byte b[8]; };
std::byte buffer[1024];
// ...
auto p = bit_cast<Word*>(&buffer[i]);   // p points to 8 bytes
auto i = bit_cast<int64_t>(*p);         // convert those 8 bytes to an integer

The standard-library type std::byte (the std:: is required) exists to represent bytes, rather than bytes known to represent characters or integers. In particular, std::byte provides only bit-wise logical operations and not arithmetic ones. Usually the best type to do bit operations on is an unsigned integer or std::byte — by best, I mean fastest and least likely to surprise:

void use(unsigned int ui) {
    int x0 = bit_width(ui);    // the smallest number of bits needed to represent ui
    unsigned int ui2 = rotl(ui, 8);   // rotate left 8 bits (note: doesn't change ui)
    int x1 = popcount(ui);     // the number of 1s in ui
}
// See also bitset (§15.3.2).

16.8 Exiting a Program

Occasionally, a piece of code encounters a problem that it cannot handle:

The standard library provides facilities for that last case:

These functions are for really serious errors. They do not invoke destructors — they do not do ordinary and proper clean-up. The various handlers are used to take actions before exiting, and such actions must be very simple because one reason for calling these exit functions is that the program state is corrupted. One reasonable and reasonably popular action: "restart the system in a well-defined state relying on no state from the current program." Another, slightly dicier but often not unreasonable: "log an error message and exit" — the catch is that the I/O system might have been corrupted by whatever caused the exit function to be called. Error handling is one of the trickiest kinds of programming; even getting cleanly out of a program can be hard. No general-purpose library should unconditionally terminate.

16.9 Advice

Here is a summary of the guidance from this chapter. All 22 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.

# Guideline §
1 A library doesn't have to be large or complicated to be useful. 16.1
2 Time your programs before making claims about efficiency. 16.2.1
3 Use duration_cast to report time measurements with proper units. 16.2.1
4 To represent a date directly in source code, use symbolic notation (e.g., November/28/2021). 16.2.2
5 If a date is a result of a computation, check for validity using ok(). 16.2.2
6 When dealing with time in different locations, use zoned_time. 16.2.3
7 Use a lambda to express minor changes in calling conventions. 16.3.1
8 Use mem_fn() or a lambda to create function objects that can invoke a member function using the traditional function call notation. 16.3.1, 16.3.2
9 Use function when you need to store something that can be called. 16.3.3
10 Prefer concepts to explicit use of type predicates. 16.4.1
11 You can write code to explicitly depend on properties of types. 16.4.1, 16.4.2
12 Prefer concepts over traits and enable_if whenever you can. 16.4.3
13 Use source_location to embed source code locations in debug and logging messages. 16.5
14 Avoid explicit use of std::move(). 16.6
15 Use std::forward() exclusively for forwarding. 16.6
16 Never read from an object after std::move()ing or std::forward()ing it. 16.6
17 Use std::byte to represent data that doesn't (yet) have a meaningful type. 16.7
18 Use unsigned integers or bitsets for bit manipulation. 16.7
19 Return an error-code from a function if the immediate caller can be expected to handle the problem. 16.8
20 Throw an exception from a function if the immediate caller cannot be expected to handle the problem. 16.8
21 Call exit(), quick_exit(), or terminate() to exit a program if an attempt to recover from a problem is not reasonable. 16.8
22 No general-purpose library should unconditionally terminate. 16.8

Retrieval Quiz

Clocks and durations

What are time_point and duration, and why is duration_cast needed?

Calendars

How do you write a date symbolically, check it, and compute with it?

Time zones

What is a zoned_time, and where do time zone names come from?

Function adaption

What are the three ways to adapt a function argument, and what problem does each solve?

Type functions and suffixes

What do the _v and _t suffixes mean, and what are concepts relative to type functions?

Conditional properties

How do you define operator-> for a Smart_pointer only when T is a class?

Type generators

What do conditional_t, enable_if_t, common_type_t, and underlying_type_t do?

source_location

How does log() capture the CALLER's location rather than its own?

std::move

What does std::move() actually do, and why is it dangerous?

std::forward

What is perfect forwarding, and what rules govern forward()?

Bit manipulation

What does bit_cast do, and why is std::byte special?

Exiting a program

How do you choose between return code, throw, and the exit functions — and what don't the exit functions do?


Notes

Time :-

<chrono> clocks / time_point / duration | day, month, year, weekday | time_zone, zoned_time

timing system_clock::now(), t1-t0 duration, duration_cast for units measure repeatedly

literals std::chrono_literals (10ms+33us)

calendars April/7/2018 or 2018y/April/7 | ok() validates | Year_month_day ↔ time_point (sys_days, days{7}) | constexpr-friendly

zones zoned_time{current_zone(), tp} | IANA db | "continent/city" names

Function adaption :-

3 ways lambda | mem_fn(&Shape::draw) | std::function

lambda mediates f(x) v/s x->f() calling styles

function stores any callable callbacks ; overhead possible free-store alloc | × overloading use overloaded ; C++23 move_only_function

Type functions :-

type function compile-time, type in/out | _v = value | _t = type (redundant suffixes)

predicates is_void/integral/floating_point/class/function/arithmetic/scalar, constructible family, is_same/base_of/convertible/iterator/invocable/has_virtual_destructor

uses constrain templates (complex Scalar) → prefer concepts (advice 10) ; optimization (memcpy if trivially copyable, ~50%) — verify first

conditional requires is_class_v<T> | concept Class = is_class_v || is_union_v

generators remove_const/add_const, remove/add_reference, enable_if_t (SFINAE), conditional_t, common_type_t, underlying_type_t, invoke_result_t

associated range_value_t, iter_value_t, iterator_t

source_location :-

source_location::current() where it appears ; file()/function_name() C-strings, line()/column() ints

default argument trick evaluated at caller log() reports caller location ; pre-C++20 __FILE__/__LINE__

move / forward :-

std::move doesn't move — casts to rvalue ref ('rvalue_cast') swap idiom

danger moved-from object reading it = crash ; avoid explicit move w/o demonstrated gain (advice 14) ; return std::move(x) redundant

moved-from state unspecified but destructible + assignable | containers → empty

std::forward perfect forwarding (lvalue/rvalue preserved) ; forwarding only ; never forward twice

Bits :-

<bit> bit_cast same-size reinterpret | bit_width, rotl, popcount

std::byte bytes as bytes bit-wise logical only, no arithmetic ; best types = unsigned ints / byte

Exiting :-

frequent + caller handles return code | infrequent / can't handle throw | serious exit

exit(x) atexit | abort() immediate | quick_exit(x) at_quick_exit | terminate() handler (default abort)

no destructors no clean-up handlers must be simple (restart / log — I/O may be corrupted)

rule 22 no general-purpose library should unconditionally terminate


Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed., Chapter 16: "Utilities." Addison-Wesley.
Reference: Chapter 1–12 Quick Reference & Glossary — keep it beside you while you study.
Recommended supplement: cppreference on date and time utilities, std::function, and type traits.

Questions? Ask your agent — your teacher — about anything unclear: when measuring time reliably, why std::move is called what it is, or which exit function fits which catastrophe. Follow-ups are expected, not optional.

Chapters

  1. Lesson 1: The Basics (Ch. 1)
  2. Lesson 2: User-Defined Types (Ch. 2)
  3. Lesson 3: Separate Compilation (Ch. 3)
  4. Lesson 4: Error Handling (Ch. 4)
  5. Lesson 5: Classes (Ch. 5)
  6. Lesson 6: Essential Operations (Ch. 6)
  7. Lesson 7: Templates (Ch. 7)
  8. Lesson 8: Concepts and Generic Programming (Ch. 8)
  9. Lesson 9: Library Overview (Ch. 9)
  10. Lesson 10: Strings and Regular Expressions (Ch. 10)
  11. Lesson 11: Input and Output (Ch. 11)
  12. Lesson 12: Containers (Ch. 12)
  13. Lesson 13: Algorithms (Ch. 13)
  14. Lesson 14: Ranges (Ch. 14)
  15. Lesson 15: Pointers and Containers (Ch. 15)
  16. Lesson 16: Utilities (Ch. 16 — this lesson)
  17. Lesson 17: Numerics (Ch. 17)
  18. Lesson 18: Concurrency (Ch. 18)
  19. Lesson 19: History and Compatibility (Ch. 19)
← Course Dashboard ← Ch. 15: Pointers & Containers Ch. 17: Numerics → Quick Reference →