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.
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.
In <chrono>, the standard library provides facilities for dealing with
time:
time_point, and duration for measuring
how long some action takes, and as the basis for anything to do with time;
day, month, year, and weekday for
mapping time_points into our everyday lives;
time_zone and zoned_time to deal with differences in time
reporting across the globe.
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
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.
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."
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:
std::mem_fn() to make a function object from a member function (§16.3.2).
std::function (§16.3.3).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.
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.
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).
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).
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.
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.
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.
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 |
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.
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.
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).
Occasionally, a piece of code encounters a problem that it cannot handle:
The standard library provides facilities for that last case:
exit(x): call functions registered with atexit(), then exit the
program with the return value x (atexit() is basically a primitive destructor
mechanism shared with the C language).
abort(): exit the program immediately and unconditionally with a return value
indicating unsuccessful termination.
quick_exit(x): call functions registered with at_quick_exit(),
then exit with the return value x.
terminate(): call the terminate_handler. The default terminate_handler is
abort().
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.
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 |
What are time_point and duration, and why is duration_cast needed?
How do you write a date symbolically, check it, and compute with it?
What is a zoned_time, and where do time zone names come from?
What are the three ways to adapt a function argument, and what problem does each solve?
What do the _v and _t suffixes mean, and what are concepts relative to type functions?
How do you define operator-> for a Smart_pointer only when T is a class?
What do conditional_t, enable_if_t, common_type_t, and underlying_type_t do?
How does log() capture the CALLER's location rather than its own?
What does std::move() actually do, and why is it dangerous?
What is perfect forwarding, and what rules govern forward()?
What does bit_cast do, and why is std::byte special?
How do you choose between return code, throw, and the exit functions — and what don't the exit functions do?
<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
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 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::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__
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
<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
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.