The C++ Standard Library includes an assortment of utility classes designed for specific tasks. You've probably seen many of these classes in other recipes in this book.
This chapter covers a broad range of utilities, including time measurement, generic types, smart pointers, and more, in the following recipes:
std::optional std::any for type safety std::variant std::chrono std::unique_ptr std::shared_ptr Introduced with C++17, the std::optional class holds an optional value .
There are any number of scenarios where this can be useful, including long-running processes where a result may not yet be available, API callbacks where a function may or may not return a value, lazy initialization where an object is constructed only when needed, or any use case where a value may or may not be valid.
Consider the case where you have a function that may or may not return a value. For example, a function that checks if a number is prime , but returns the first factor if there is one. This function should return either a value or a bool status. We could create a struct that carries both value and status:
struct factor_t { bool is_prime; long factor; }; factor_t factor(long n) { factor_t r{}; for(long i = 2; i <= n / 2; ++i) { if (n % i == 0) { r.is_prime = false; r.factor = i; return r; } } r.is_prime = true; return r; } It's a clumsy solution, but it works and it's not uncommon.
This is made much simpler with the optional class:
optional<long> factor(long n) { for (long i = 2; i <= n / 2; ++i) { if (n % i == 0) return {i}; } return {}; } Now we just return a value, or a not value.
We can call it like this:
long a {42}; long b {73}; auto x = factor(a); auto y = factor(b); if (x) println("lowest factor of {} is {}", *a, *x); else println("{} is prime", *a); if (y) println("lowest factor of {} is {}", *b, *y); else println("{} is prime", *b); Our output is:
lowest factor of 42 is 2 73 is prime The optional class allows us to easily return both status and value.
In this recipe we'll look at some examples of how to use the optional class:
optional<int> a {42}; println("{}", *a); We access the value of the optional with the pointer dereference operator * .
Output:
42 if (a) println("{}", *a); else println("no value "); If a were constructed without a value:
optional<int> a {}; The output would reflect the else condition:
no value using oint = std::optional<int>; oint a {42}; oint b {73}; If we want to operate on oint objects, with oint objects as the result, we can provide operator overloads:
oint operator+(const oint& a, const oint& b) { if (a && b) return *a + *b; return {}; } It may also be useful to provide an overload for the optional operator with a non- optional operand:
oint operator+(const oint& a, const int b) { if (a) return *a + b; return {}; } This same technique would apply for any of the common arithmetic operators.
Now we can operate on the oint objects directly:
auto sum {a + b}; if (sum) { println("{} + {} = {}", *a, *b, *sum); } else { println("no value"); } Output:
42 + 73 = 115 Notice that when we declare b with the default constructor:
oint b {}; The result is now an optional with no value. The if(sum) test returns false , and we get the else branch output:
no value The std::optional class is made for simplicity. It provides operator overloads for many common functions. It also includes member functions for further flexibility.
The optional class provides an operator bool overload for determining if the object has a value:
optional<int> n{ 42 }; if (n) ... // has a value Or you may use the has_value() member function:
if (n.has_value()) ... // has a value To access the value, you may use the operator* overload:
x = *n; // * retruns the value Or you may use the value() member function:
x = n.value(); // * retruns the value The reset() member function destroys the value and resets the state of the optional object.
n.reset(); // no longer has a value The optional class provides exception support with the value() method:
b.reset(); try { println("{}", b.value()); } catch(const std::bad_optional_access& e) { println("b.value(): {}", e.what()); } Output:
b.value(): bad optional access Only the value() method throws an exception. The behavior of the * operator is undefined for an invalid value.
Introduced with C++17, the std::any class provides a type-safe container for a single object of any type.
For example, this is a default-constructed any object:
any x {}; This object has no value. We can test that with the has_value() method:
if (x.has_value()) println("have value "); else println("no value"); Output:
no value We assign a value to the any object with the assignment operator:
x = 42; Now the any object has a value and a type:
if (x.has_value()) { println("x has type: {}", x.type().name()); println("x has value: {}", any_cast<int>(x)); } else { println("no value");; } Output:
x has type: i x has value: 42 The type() method returns a type_info object. The type_info::name() method returns an implementation-defined name for the type in a C-string. In this case, for GCC, the i means int .
We use the non-member function any_cast< type >() to cast the value for use.
We can re-assign the any object with different values of different types:
using namespace std::literals; x = "abc"s; println("x is type {} with value {}", x.type().name(), any_cast<string>(x)); Notice the use of the suffix after the string "abc"s . This uses the std::literals namespace to create a string object from a literal C-string.
Output:
x is type NSt7__cxx1112basic_string... with value abc I've abbreviated the long type-name from GCC but you get the idea. The same any object that once held an int now contains an STL string object.
The main usefulness of the any class is in creating a polymorphic function. Let's examine how to do that in this recipe:
In this recipe we'll build a polymorphic function using the any class. A polymorphic function is one that can take objects of different types in its parameters.
any object and prints its type and value: void p_any(const any& a) { if (!a.has_value()) { println("None."); } else if (a.type() == typeid(int)) { println("int: {}", any_cast<int>(a)); } else if (a.type() == typeid(string)) { println("string: \"{}\"", any_cast<const string&>(a)); } else if (a.type() == typeid(list<int>)) { print("list<int>: "); for (auto& i : any_cast<const list<int>&>(a)) print("{} ", i); print("\n"); } else { println("something else: {}", a.type().name()); } } The p_any() function first tests to see if the object has a value. It then tests the type() method against various types and takes appropriate action for each type.
Before the any class, we would have had to write four different specializations for this function and we still wouldn't be able to easily handle the default case.
main() like this: p_any({}); p_any(47); p_any("abc"s); p_any(any(list{ 1, 2, 3 })); p_any(any(vector{ 1, 2, 3 })); Output:
None. int: 47 string: "abc" list<int>: 1 2 3 something else: St6vectorIiSaIiEE Our polymorphic function handles the various types with a minimum of code.
The std::any copy constructor and assignment operator use direct initialization to make a non- const copy of the target object as the contained object. The type of the contained object is stored separately as a typeid object.
Once initialized, the any object has the following methods:
emplace() replaces the contained object, constructing the new object in place reset() destroys the contained object has_value() returns true if there is a contained object type() returns a typeid object, representing the type of the contained object operator=() replaces the contained object by copy or move The any class also supports the following non-member functions:
any_cast<T>() is a template function that provides type-safe access to the contained object Keep in mind that the any_cast<T>() function returns a copy of the contained object. You may use any_cast<T&>() to return a reference.
std::swap() specializes the std::swap algorithm If you try to cast an any object with an incompatible type, it throws a bad_any_cast exception:
X = std::string("foo"); try { println("{}", any_cast<int>(x)); } catch (std::bad_any_cast& e) { println("any: {}", e.what()); } Output:
any: bad any_cast It's worth understanding that std::any::any_cast<>() is typed at compile time. This is why it requires a type as a template argument. While std::any supports run-time inspection through its type() method, extraction still requires the destination type to be known at compile time.
Introduced with C++17, the std::variant class may hold different values, one at a time, where each value must fit in the same allocated memory space. It's useful for holding alternative types for use in a single context.
The variant class is a tagged union . It differs from the union primitive in that only one type may be in effect at a time.
The primitive union type, inherited from C, provides shared storage where the same storage may be accessed as different types. For example:
union ipv4 { uint32_t int32; struct { uint8_t a, b, c, d; } quad; } addr; addr.int32 = 0x2A05A8C0; println("ip addr dotted quad: {}.{}.{}.{} ", addr.quad.a, addr.quad.b, addr.quad.c, addr.quad.d); println("ip addr int32 (LE): {:08X} ", addr.int32); Output:
ip addr dotted quad: 192.168.5.42 ip addr int32 (LE): 2A05A8C0 In this example, the union has two members, a struct , and a uint32_t , where the struct has four uint8_t members. This gives us two different interpretations of the same 32-bit memory space. We can view the same IPv4 address as either a 32-bit unsigned integer, or four 8-bit unsigned integers in the common dotted quad notation . This provides a form of bitwise polymorphism that can be useful at the systems level.
Because this union example operates on the byte level, it relies on the endianness of the underlying machine. For example, a 32-bit integer value is commonly stored as four 8-bit bytes. In a little-endian system the bytes are stored least-significant byte first. On a big-endian system the order is reversed.
This code will behave as expected on little-endian machines but will give different results on big-endian systems. While IPv4 addresses are universally transmitted in network byte order (big-endian), they are commonly stored in the native byte order of the host system. Most modern systems use little-endian representation, yet notable exceptions remain, such as the IBM/360 family of mainframes.
Unlike the C standard, the C++ standard specifies that only the most recently written member of a union is considered active. Reading any other member is formally considered undefined behavior. In practice, however, this form of type-punning is widely supported by mainstream compilers and remains common in systems-level code.
The std::variant class is different from a union in that it's a tagged union , where each value is tagged with its type. If we store a value as uint32_t , we may only access it as uint32_t . This makes variant type safe, but not a replacement for union .
In this recipe we demonstrate the use of std::variant with a small catalog of household pets of various species.
Animal : class Animal { string_view a_name{}; string_view a_sound{}; Animal(); public: Animal(string_view n, string_view s) : a_name {n}, a_sound {s} {} void speak() const { println("{} says {}", a_name, a_sound); } void sound(string_view s) { a_sound = s; } }; The name of the Animal and the sound that it makes are passed in the constructor.
class Cat : public Animal { public: Cat(string_view n) : Animal(n, "meow") {} }; class Dog : public Animal { public: Dog(string_view n) : Animal(n, "arf!") {} }; class Wookie : public Animal { public: Wookie(string_view n) : Animal(n, "grrraarrgghh!") {} }; Each of these classes set the sound for their specific species by calling the parent constructor.
using v_animal = std::variant<Cat, Dog, Wookie>; This variant can hold any of the types, Cat , Dog , or Wookie .
main() , we create a list using our v_animal alias as the type: int main() { list<v_animal> pets { Cat {"Hobbes"}, Dog {"Fido"}, Cat {"Max"}, Wookie {"Chewie"} }; Each element in the list is of a type included in the variant definition.
std::visit() function. The std::visit() function is defined in the variant header. It calls a functor with an object contained in the variant . First, let's define a functor that accepts any of our pets:
struct animal_speaks { void operator()(const Dog& d) const { d.speak(); } void operator()(const Cat& c) const { c.speak(); } void operator()(const Wookie& w) const { w.speak(); } }; This is a simple functor class with overloads for each of the Animal sub-classes. We call it via visit() with each of our list elements:
for (const v_animal& a : pets) { visit(animal_speaks{}, a); } We get this output:
Hobbes says meow Fido says arf! Max says meow Chewie says grrraarrgghh! variant class also provides an index() method: for (const v_animal &a : pets) { auto idx {a.index()}; if (idx == 0) get<Cat>(a).speak(); if (idx == 1) get<Dog>(a).speak(); if (idx == 2) get<Wookie>(a).speak(); } Output:
Hobbes says meow Fido says arf! Max says meow Chewie says grrraarrgghh! Each variant object is indexed, based on the order in which the types were declared in the template arguments. Our v_animal type was defined with std::variant<Cat, Dog, Wookie> , and these types are indexed as 0 – 2 in that order.
get_if<T>() function tests a given element against a type: for (const v_animal& a : pets) { if (const auto c {get_if<Cat>(&a)}; c) { c->speak(); } else if (const auto d {get_if<Dog>(&a)}; d) { d->speak(); } else if (const auto w {get_if<Wookie>(&a)}; w) { w->speak(); } } Output:
Hobbes says meow Fido says arf! Max says meow Chewie says grrraarrgghh! The get_if<T>() function returns a pointer if the type of the element matches T , otherwise it returns nullptr .
holds_alternative<T>() function returns true or false . We can use this to test a type against an element, without returning the element: size_t n_cats{}, n_dogs{}, n_wookies{}; for(const v_animal& a : pets) { if(holds_alternative<Cat>(a)) ++n_cats; if(holds_alternative<Dog>(a)) ++n_dogs; if(holds_alternative<Wookie>(a)) ++n_wookies; } println("there are {} cat(s), " "{} dog(s), " "and {} wookie(s)", n_cats, n_dogs, n_wookies); Output:
there are 2 cat(s), 1 dog(s), and 1 wookie(s) The std::variant class is a single-object container. An instance of variant<X, Y, Z> must hold exactly one object of type X , Y , or Z . It holds both the value and the type of its current object.
The index() method tells us the type of the current object:
if (v.index() == 0) // if variant is type X The holds_alternative<T>() non-member function returns true if T is the type of the current object.
if (holds_alternative<X>(v)) // if current variant obj is type X We can retrieve the current object with the get() non-member function:
auto o {get<X>(v)}; // current variant obj must be type X We can combine the test for type and retrieval with the get_if() non-member function:
auto* p {get_if<X>(v)}; // nullptr if current obj not type X The visit() non-member function invokes a callable object with the current variant object as its single parameter:
visit(f, v); // calls f(v) with current variant obj The visit() function is the only way to retrieve an object without testing its type. In combination with a functor that can handle each type, this can be very flexible:
struct animal_speaks { void operator()(const Dog& d) const {d.speak();} void operator()(const Cat& c) const {c.speak();} void operator()(const Wookie& v) const {v.speak();} }; main() { for (const v_animal& a : pets) { visit(animal_speaks{}, a); } } Output:
Hobbes says meow Fido says arf! Max says meow Chewie says grrraarrgghh! The std::chrono library provides tools for measuring and reporting time and intervals.
The chrono library can be used for working with dates and times, measuring durations, and other time-related purposes.
Using the chrono library, this recipe explores techniques for timing events.
The system_clock class is used for reporting current date and time. The steady_clock and high_resolution_clock classes are used for timing events. Let's look at the differences between these clocks.
using std::chrono::system_clock; using std::chrono::steady_clock; using std::chrono::high_resolution_clock; using std::chrono::duration; using seconds = duration<double>; using milliseconds = duration<double, std::milli>; using microseconds = duration<double, std::micro>; using fps24 = duration<unsigned long, std::ratio<1, 24>>; The duration class represents an interval between two points in time. These aliases are convenient for using different intervals.
system_clock class: auto t = system_clock::now(); println("system_clock::now is {:%F %T %Z}", t); The system_clock::now() function returns a time_point object. The <chrono> library includes a format() specialization for time_point that uses strftime() format specifiers.
The output is:
system_clock::now is 2025-12-26 19:23:21.412144 UTC The number of digits after the decimal will vary on different systems.
We can also use system_clock to time an event. First, we need something to time. Here's a function that counts primes:
constexpr uint64_t MAX_PRIME {0x8FFFFF}; uint64_t count_primes() { constexpr auto isprime = [](uint64_t n) { if (n == 2) return true; if (n < 2 || n % 2 == 0) return false; for (uint64_t i{3}; i <= n / i; i += 2) { if (n % i == 0) return false; } return true; }; uint64_t count {MAX_PRIME >= 2 ? 1 : 0}; // count 2 uint64_t start {3}; uint64_t end {MAX_PRIME}; for (uint64_t i {start}; i <= end; i += 2) { if (isprime(i)) ++count; } return count; } This function counts the prime numbers between 3 and 0x8FFFFF (9,437,183), which should take a few seconds on most modern systems.
count_primes() : seconds timer(uint64_t(*f)()) { auto t1 {system_clock::now()}; uint64_t count{ f() }; auto t2 {system_clock::now()}; seconds secs {t2 - t1}; println("there are {} primes in range", count); return secs; } This function takes a function argument f and returns a duration<double> . We use steady_clock::now() to mark the time before and after the call to f() . We take the difference between the two times and return it in a duration object.
timer() from main() like this: int main() { auto secs {timer(count_primes)}; println("time elapsed: {:.3f} sec", secs.count()); ... This passes the count_primes() function to timer() and stores the duration object in secs.
Output:
there are 629,686 primes in range time elapsed: 12.928 seconds The count() method on the duration object returns the duration in the specified units, in this case, a double representing seconds of duration.
This result was produced on a VM running Debian with GCC. The exact time will vary on different systems.
system_clock class is designed to provide the current wall clock time. While its resolution may support timing purposes, it is not guaranteed to be monotonic . In other words, it may not always provide consistent ticks (timing intervals). The chrono library provides a clock more suitable for timing events in steady_clock . It has the same interface as system_clock , but provides more reliable ticks for timing purposes:
seconds timer(uint64_t(*f)()) { auto t1 {steady_clock::now()}; uint64_t count {f()}; auto t2 {steady_clock::now()}; seconds secs {t2 - t1}; println("there are {} primes in range", count); return secs; } steady_clock is designed to provide reliably consistent, monotonic ticks, suitable for timing events. It uses a relative time reference, so it's not useful for wall clock time. While system_clock measures beginning with a fixed point in time (e.g., 1 January 1970, 00:00 UTC), steady_clock uses a relative time.
Another option is high_resolution_clock , which provides the shortest tick period available on a given system, but is not implemented consistently across different systems. It may be an alias for system_clock or steady_clock , and it may or may not be monotonic. high_resolution_clock is not recommended for general purpose use.
timer() function returns seconds , which is an alias for duration<double> : using seconds = duration<double>; The duration class takes an optional second template parameter, a ratio:
template<class Rep, class Period = std::ratio<1>> class duration; The <chrono> header provides convenience types for many decimal ratios, including milli and micro :
using milliseconds = duration<double, std::milli>; using microseconds = duration<double, std::micro>; If we require something else, we may provide our own. For example:
using fps24 = duration<unsigned long, std::ratio<1, 24>>; fps24 represents the number of frames of film shot at the standard 24 frames per second. The ratio is 1/24 of a second.
This allows us to easily convert between different ranges of duration:
println("time elapsed: {:.3f} sec", secs.count()); println("time elapsed: {:.3f} ms", milliseconds(secs).count()); println("time elapsed: {:.3e} μs", microseconds(secs).count()); println("time elapsed: {} frames at 24 fps", floor<fps24>(secs).count()); Output:
time elapsed: 12.928 sec time elapsed: 12927.994 ms time elapsed: 1.293e+07 μs time elapsed: 310 frames at 24 fps Because the fps24 alias uses unsigned long instead of double , a type conversion is required. The floor function provides this by discarding the fractional part. round() and ceil() are also available in this context.
chrono library provides format() specializations for the standard duration ratios: println("time elapsed: {:.3}", secs); println("time elapsed: {:.3}", milliseconds(secs)); println("time elapsed: {:.3}", microseconds(secs)); Output:
time elapsed: 12.928s time elapsed: 12928ms time elapsed: 1.2928e+07μs These results will, of course, vary on different systems.
There are two major pieces of the chrono library at work here: the various clock classes and the duration class.
The clock classes include:
system_clock – provides wall clock time. steady_clock – provides guaranteed monotonic ticks for duration measurements high_resolution_clock – provides the shortest available tick period. May be an alias for system_clock or steady_clock on some systems. We use system_clock to display the current time and date. We use steady_clock to measure intervals.
Each of the clock classes has a now() method that returns a time_point , representing the current value of the clock. now() is a static member function, so it's called on the class without instantiating an object:
auto t1 {steady_clock::now()}; The duration class is used to hold a time interval, that is, the difference between two time_point objects. It is generally constructed with a time_point object's subtraction operator.
duration<double> secs {t2 - t1}; The time_point subtraction operator doubles as a constructor for a duration object:
template<class C, class D1, class D2> constexpr duration<D1,D2> operator-(const time_point<C,D1>& pt_lhs, const time_point<C,D2>& pt_rhs); The duration class has template parameters for type-representation and a ratio object:
template<class Rep, class Period = std::ratio<1>> class duration; The Period template parameter defaults to a ratio of 1:1, which is seconds .
The library provides ratio aliases (such as micro , milli , etc.) for powers-of-ten from atto (1/1,000,000,000,000,000,000) through exa (1,000,000,000,000,000,000/1). This allows us to create standard durations as we did in our example:
using milliseconds = duration<double, std::milli>; using microseconds = duration<double, std::micro>; The count() method gives us the duration in Rep type:
constexpr Rep count() const; This allows us to easily access the duration for display or other purposes:
println("duration: {}", secs.count()); The std::tuple class is essentially a more complex, less convenient, struct . The interface for tuple is cumbersome, although class template argument deduction and structured binding has made it somewhat easier.
The one real advantage of tuple is that it can be used with fold expressions to support varying numbers of elements of varying types.
Designed to make it easier to expand a variadic parameter pack, fold expressions were introduced with C++17. Prior to fold expressions, expanding a parameter pack required a recursive function:
template<typename T> void f(T final) { print("\n"); } template<typename T, typename... Args> void f(T first, Args... args) { print("{}", first); f(args...); } int main() { f("hello", ' ', 47, ' ', "world"); } Output:
hello 47 world Using a fold expression this is much simpler:
template<typename... Args> void f(Args... args) { (std::print("{}", args), ...); } Output:
hello 47 world There are four types of fold expressions:
(args op ...) (... op args) (args op ... op init) (init op ... op args) The expression in the example above is a unary right fold .
(std::print("{}", args), ...); It's unary because there is one operator, the comma, and it's a right fold because the ellipsis is to the right of the operator.
This expands to:
print("{}", "hello"); print("{}", ' '); print("{}", 47); print("{}", ' '); print("{}", "world"); Fold expressions are a great convenience for many purposes. Let's look at how we can use them with tuples.
In this recipe we'll create a template function that operates on a tuple with varying numbers and types of elements.
tuple of unknown size and type and prints each element with print() : template<typename... T> void print_t(const std::tuple<T...>& tup) { std::apply([](const auto&... elems) { ((std::print("{} ", elems)), ...); }, tup); std::print("\n"); } This function uses std::apply to translate the tuple into a series of arguments passed to the lambda. Then, within the lambda, we use a unary right fold expression to call print() with each argument.
We could use a separate function in place of the lambda, but I prefer to keep it in a single scope.
main() with a variety of tuples of a mixture of types: int main() { tuple lables {"ID", "Name", "Scale"}; tuple employee {123456, "John Doe", 3.7}; tuple nums {1, 7, "forty-two", 47, 73L, -111.11}; print_t(lables); print_t(employee); print_t(nums); } The fold expression operates with however many elements, and however many different types. Each parameter is passed to the print() function with its type intact, allowing the formatter to apply the correct formatting.
Output:
ID Name Scale 123456 John Doe 3.7 1 7 forty-two 47 73 -111.11 The challenge with tuple is its restrictive interface. You can retrieve elements with std::tie() , with structured bindings , or the std::get<> function. None of these techniques are useful if you don't know the number and type of elements in the tuple .
We get around this limitation by using the std::apply() function:
template< class F, class Tuple > constexpr decltype(auto) apply( F&& f, Tuple&& t ); The std::apply() function takes a function argument f and a tuple t . It calls the function with each of the arguments of the tuple. This effectively unpacks the tuple and passes its elements as fully typed arguments to the function.
We could pass a function, functor, or lambda to apply() . I chose a lambda for its simplicity and to keep everything in the scope of the function:
std::apply([](const auto&... elems) { ((std::print("{} ", elems)), ...); }, tup); This keeps the solution simple and self-contained.
The unary right fold expression takes each element of the lambda's parameter pack and passes it to the print() function.
Keep in mind that, as with template functions in general, the compiler will generate a separate specialization of this function for each combination of tuple parameters.
We can use this technique for other tasks. For example, here's a function that returns the sum of all the int values in a tuple of unknown size:
template<typename... T> constexpr int sum_t(const tuple<T...>& tup) { return std::apply([](const auto&... elems) { return (elems + ...); }, tup); } This uses the same technique as the example in our recipe, using std::apply to unpack the tuple and pass its elements as parameters to a lambda. Then we can use a simple unary right fold with a + operator to return the sum of the elements.
We can call this with several tuple objects of varying numbers of int values:
tuple ti1 {1, 2, 3, 4, 5}; tuple ti2 {9, 10, 11, 12, 13, 14, 15}; tuple ti3 {47, 73, 42}; auto sum1 {sum_t(ti1)}; auto sum2 {sum_t(ti2)}; auto sum3 {sum_t(ti3)}; println("sum of ti1: {}", sum1); println("sum of ti2: {}", sum2); println("sum of ti3: {}", sum3); Output:
sum of ti1: 15 sum of ti2: 84 sum of ti3: 162 Smart pointers are an excellent tool for managing allocated heap memory .
Heap memory is managed at the lowest level by the C functions malloc() and free() . malloc() allocates a block of memory from the heap, and free() returns it to the heap. These functions do not perform initialization and they do not call constructors or destructors. If you fail to return allocated memory to the heap with a call to free() , the behavior is undefined and often leads to memory leaks and security vulnerabilities.
C++ provides the new and delete operators to allocate and free heap memory in place of malloc() and free() . The new and delete operators call object constructors and destructors but still do not manage memory. If you allocate memory with new and fail to free it with delete , you will likely get memory leaks.
Introduced with C++14, smart pointers comply with the RAII idiom, Resource Acquisition Is Initialization . This means that when memory is allocated for an object, that object's constructor is called, and when the object's destructor is called, the memory is automatically returned to the heap.
For example, when we create a new smart pointer with make_unique() :
{ // beginning of scope auto p = make_unique<Thing>(); // memory alloc'd, ctor called, process_thing(p); // p is unique_ptr<Thing> } // end of scope, dtor called, memory freed make_unique() allocates memory for a Thing object, calls the Thing default constructor, constructs a unique_ptr<Thing> object, and returns the unique_ptr . When p goes out of scope, the Thing destructor is called, and the memory is automatically returned to the heap.
Aside from the memory management, a smart pointer works very much like a primitive pointer:
auto x = *p; // *p derefs the pointer, returns Thing object auto y = p->thname; // p-> derefs the pointer, returns member unique_ptr is a smart pointer that allows only one instance of the pointer. It may be moved but it may not be copied. Let's take a closer look at how to use unique_ptr .
In this recipe we examine std::unique_ptr with a demonstration class that prints when its constructors and destructor are called.
class Thing { string_view thname {"unk"}; public: Thing() { println("default ctor: {}", thname); } Thing(const string_view& n) : thname(n) { println("param ctor: {}", thname); } ~Thing() { println("dtor: {}", thname); } string_view name() const { return thname; } }; This class has a default constructor, a parameterized constructor, and a destructor. Each of these has a simple print statement to tell us what was called. I've also included a getter to return the name of the Thing .
unique_ptr by itself, it does not allocate memory or construct a managed object: int main() { unique_ptr<Thing> p1; println("end of main()"); } Output:
end of main() new operator, it allocates memory and constructs a Thing object: int main() { unique_ptr<Thing> p1 {new Thing}; println("end of main()"); } Output:
default ctor: unk end of main() dtor: unk The new operator constructs a Thing object by calling the default constructor.
Notice that the "end of main()" message occurs before the dtor message. The unique_ptr<Thing> destructor calls the Thing destructor when the smart pointer reaches the end of its scope, which is after the "end of main()" message.
The Thing default constructor does not initialize the thname string, leaving its default value, "unk" .
make_unique() to get the same result: int main() { auto p1 = make_unique<Thing>(); println("end of main()"); } Output
default ctor: unk end of main() dtor: unk The make_unique() helper function takes care of the memory allocation and returns a unique_ptr object. This is the recommended way to construct a unique_ptr .
make_unique() are used in constructing the target object: int main() { auto p1 = make_unique<Thing>("Thing 1") }; println("end of main()"); } Output:
param ctor: Thing 1 end of main() dtor: Thing 1 The parameterized constructor assigns a value to thname so our Thing object is now "Thing 1" .
unique_ptr<Thing> argument: void process_thing(unique_ptr<Thing> p) { if (p) println("processing: {}", p->name()); else println("invalid pointer"); } If we try to pass a unique_ptr to this function, we get a compiler error:
process_thing(p1); Compiler error:
error: use of deleted function... This fails because the call by value argument to the function call tries to make a copy of the unique_ptr object, but the unique_ptr copy constructor is deleted to prevent copying. The solution is to have the function take a const& reference:
void process_thing(const unique_ptr<Thing>& p) { if (p) println("processing: {}", p->name()); else println("invalid pointer"); } Output:
param ctor: Thing 1 processing: Thing 1 end of main() dtor: Thing 1 The const& reference parameter maintains the single instance the unique_ptr object as intended.
process_thing() with a temporary object, which is immediately destroyed at the end of the function scope: int main() { auto p1 {make_unique<Thing>("Thing 1")}; process_thing(p1); process_thing(make_unique<Thing>("Thing 2")); println("end of main()"); } Output:
param ctor: Thing 1 processing: Thing 1 param ctor: Thing 2 processing: Thing 2 dtor: Thing 2 end of main() dtor: Thing 1 A smart pointer is simply an object that presents a pointer interface, while owning and managing the resources of another object.
The unique_ptr class is distinguished by its deleted copy constructor and copy assignment operator, which prevents the smart pointer from being copied.
You may not copy a unique_ptr :
auto p2 = p1; Compiler error:
error: use of deleted function... But you can move a unique_ptr :
auto p2 = std::move(p1); process_thing(p1); process_thing(p2); After the move, p1 is invalid and p2 now owns the object which was initialized as "Thing 1 . "
Output:
invalid pointer processing: Thing 1 end of main() dtor: Thing 1 The unique_ptr interface has a reset() method which calls the object destructor and invalidates the pointer:
p1.reset(); // pointer is now invalid process_thing(p1); Output:
dtor: Thing 1 invalid pointer The reset() method may also be used to replace the managed object with another of the same type:
p1.reset(new Thing("Thing 3")); process_thing(p1); Output:
param ctor: Thing 3 dtor: Thing 1 processing: Thing 3 The std::shared_ptr class is a smart pointer that owns its managed object and maintains a use counter to keep track of the number of references to the object so that the destructor may be called when the last copy of the shared_ptr is destroyed. This is distinct from a traditional "garbage collection" scheme which attempts to discover and reclaim unreachable objects automatically. This recipe explores the use of shared_ptr to manage memory while sharing ownership of the pointer.
For more detail about smart pointers, see the introduction to the recipe "Manage allocated memory with std::unique_ptr ", earlier in this chapter.
In this recipe we examine std::shared_ptr with a demonstration class that prints when its constructors and destructor are called.
class Thing { string_view thname{"unk"}; public: Thing() { println("default ctor: {}", thname); } Thing(const string_view& n) : thname(n) { println("param ctor: {}", thname); } ~Thing() { println("dtor: {}", thname); } string_view name() const { return thname; } }; This class has a default constructor, a parameterized constructor, and a destructor. Each of these has a simple print() statement to tell us what was called.
The shared_ptr class works very much like other smart pointers, in that it may be constructed with the new operator or with its helper make_shared() function:
int main() { shared_ptr<Thing> p1 {new Thing("Thing 1")}; auto p2 = make_shared<Thing>("Thing 2"); println("end of main()"); } Output:
param ctor: Thing 1 param ctor: Thing 2 end of main() dtor: Thing 2 dtor: Thing 1 The make_shared() function is recommended as it manages the construction process and is less prone to error.
As with the other smart pointers, the managed object is destroyed and its memory is returned to the heap when the pointer goes out of scope. In the case of main() , this happens after the last line of the function, which prints "end of main()" .
shared_pointer class keeps a counter to monitor the number of copies of the pointer. Here's a function to check the use count of a shared_ptr object: void check_thing_ptr(const shared_ptr<Thing>& p) { if (p) println("{} use count: {}", p->name(), p.use_count()); else println("invalid pointer"); } name() is a function member of the Thing class, so we access it through the pointer with the member dereference p‑> operator. The use_count() function is a member of the shared_ptr class, so we access it with the object member p. operator.
Let's call this with our pointers:
check_thing_ptr(p1); check_thing_ptr(p2); Output:
Thing 1 use count: 1 Thing 2 use count: 1 println("make 4 copies of p1: "); auto pa = p1; auto pb = p1; auto pc = p1; auto pd = p1; check_thing_ptr(p1); Output:
make 4 copies of p1: Thing 1 use count: 5 check_thing_ptr(pa); check_thing_ptr(pb); check_thing_ptr(pc); check_thing_ptr(pd); Output:
Thing 1 use count: 5 Thing 1 use count: 5 Thing 1 use count: 5 Thing 1 use count: 5 Each pointer reports the same use count.
{ // new scope cout << "make 4 copies of p1:\n"; auto pa = p1; auto pb = p1; auto pc = p1; auto pd = p1; check_thing_ptr(p1); } // end of scope check_thing_ptr(p1); Output:
make 4 copies of p1: Thing 1 use count: 5 Thing 1 use count: 1 { cout << "make 4 copies of p1:\n"; auto pa = p1; auto pb = p1; auto pc = p1; auto pd = p1; check_thing_ptr(p1); pb.reset(); p1.reset(); check_thing_ptr(pd); } // end of scope Output:
make 4 copies of p1: Thing 1 use count: 5 Thing 1 use count: 3 dtor: Thing 1 Destroying pb (a copy) and p1 (the original) leaves three copies of the pointer ( pa , pc , and pd ), so the managed object remains.
The remaining three pointer copies are destroyed at the end of the scope in which they were created. Then the object is destroyed and its memory returned to the heap.
It's important to understand that the std::shared_ptr class makes it possible for two objects to refer to each other, creating a condition known as circular references . The solution is to use a std::weak_ptr for one of the references. We'll discuss this in the next recipe, "Use weak pointers with shared objects".
The shared_ptr class is distinguished by its management of multiple pointers to the same managed object.
The shared_ptr object's copy constructor and copy assignment operator increment a use counter . The destructor decrements the use counter until it reaches zero, then destroys the managed object and returns its memory to the heap.
The shared_ptr class manages both the managed object and a heap-allocated control block . The control block contains the use counter, along with other housekeeping objects. The control block is managed and shared between copies along with the managed object. This allows the original shared_ptr object to cede control to its copies, so that the last remaining shared_ptr may manage the object and its memory.
Strictly speaking, std::weak_ptr is not a smart pointer. Rather, it's an observer that operates in cooperation with a shared_ptr . A weak_ptr object does not hold a pointer on its own.
There are circumstances where shared_ptr objects may create dangling pointers or race conditions, which could lead to memory leaks or other problems. The solution is to use weak_ptr objects with the shared_ptr .
In this recipe we examine the use of std::weak_ptr with std::shared_ptr , using a demonstration class that prints when its constructors and destructor are called.
shared_ptr and unique_ptr : class Thing { string_view thname {"unk"}; public: Thing() { println("default ctor: {}", thname); } Thing(const string_view& n) : thname(n) { println("param ctor: {}", thname); } ~Thing() { println("dtor: {}", thname); } string_view name() const { return thname; } }; This class has a default constructor, a parameterized constructor, and a destructor. Each of these has a simple print() statement to tell us what was called.
weak_ptr object: void get_weak_thing(const weak_ptr<Thing>& p) { if (auto sp = p.lock()) { println("{}: count {}", sp->name(), p.use_count()); } else println("no shared object"); } A weak_ptr does not operate as a pointer on its own, it requires the use of a shared_ptr . The lock() function returns a shared_ptr object, which can then be used to access the managed object.
weak_ptr requires an associated shared_ptr , we'll start main() by creating a shared_ptr<Thing> object. When we create a weak_ptr object without assigning the shared_ptr , the expired flag is initially set: int main() { auto thing1 = make_shared<Thing>("Thing 1"); weak_ptr<Thing> wp1; println("expired: {}", wp1.expired()); get_weak_thing(wp1); println("\nend of main()"); } Output:
param ctor: Thing 1 expired: true no shared object end of main() dtor: Thing 1 The make_shared() function allocates memory and constructs a Thing object.
The weak_ptr<Thing> declaration constructs a weak_ptr object without assigning a shared_ptr . So, when we check the expired flag, it's true , indicating that there is no associated shared_ptr .
The get_weak_thing() function is unable to obtain a lock because there is no shared_ptr available.
wp1 = thing1; get_weak_thing(wp1); Output:
Thing 1: count 2 The get_weak_thing() function is now able to obtain a lock and access the managed object. The lock() method returns a shared_ptr , and the use_count() reflects the fact that there is now a second shared_ptr managing the Thing object.
The new shared_ptr is destroyed at the end of the get_weak_thing() scope.
weak_ptr class has a constructor that takes a shared_ptr for one-step construction: weak_ptr<Thing> wp2(thing1); get_weak_thing(wp2); Output:
Thing 1: count 2 The use_count() is 2 again. Remember that the previous shared_ptr was destroyed when its enclosing get_weak_thing() scope ended.
When we reset the shared_ptr , it's associated weak_ptr objects are expired:
thing1.reset(); get_weak_thing(wp1); get_weak_thing(wp2); Output:
dtor: Thing 1 no shared object no shared object After the reset() , the use count reaches zero and the managed object is destroyed and the memory released.
A weak_ptr object is an observer that holds a non-owning reference to a shared_ptr object. The weak_ptr observes the shared_ptr so that it knows when the managed object is and is not available. This allows use of a shared_ptr in circumstances where you may not always know if the managed object is active.
The weak_ptr class has a use_count() function that returns the use count of the shared_ptr , or zero if the managed object has been deleted.
long use_count() const noexcept; weak_ptr also has an expired() function that reports if the managed object has been deleted:
bool expired() const noexcept; The lock() function is the preferred way to access the shared pointer. It checks expired() to see if the managed object is available. If so, it returns a new shared_ptr that shares ownership with the managed object. Otherwise, it returns an empty shared_ptr . It does all that as one atomic operation:
std::shared_ptr<T> lock() const noexcept; One important use case for a weak_ptr is when there's a possibility of circular references to shared_ptr objects. For example, consider the case of two classes that link to each other:
struct circB; // forward declaration struct circA { shared_ptr<circB> p; ~circA() { println("dtor A"); } }; struct circB { shared_ptr<circA> p; ~circB() { println("dtor B"); } }; We have print() statements in the destructors so we can see when the objects are destroyed. We can now create two objects that point to each other with shared_ptr :
int main() { auto a{ make_shared<circA>() }; auto b{ make_shared<circB>() }; a->p = b; b->p = a; println("\nend of main()"); } When we run this, notice that the destructors are never called:
end of main() Because the objects maintain shared pointers that refer to each other, the use counts never reach zero and the managed objects are never destroyed.
We can resolve this problem by changing one of the classes to use a weak_ptr :
struct circB { weak_ptr<circA> p; ~circB() { println("dtor B "); } }; The code in main() remains the same, and we get this output:
end of main() dtor A dtor B By changing one shared_ptr to weak_ptr , we have resolved the circular reference, and the objects are now properly destroyed at the end of their scope.
The std::shared_ptr class provides an aliasing constructor to share a pointer managed by another unrelated pointer:
shared_ptr( shared_ptr<Y>&& ref, element_type* ptr ) noexcept; This returns an aliased shared_ptr object that uses the resources of ref but returns a pointer to ptr . The use_count and the deleter are shared with ref , but get() returns ptr . This allows us to share a member of a managed object without sharing the entire object, and without allowing the entire object to be deleted while we're still using the member.
In this recipe we create a managed object and share members of that object.
struct Animal { string name {}; string sound {}; Animal(const string& n, const string& a) : name{n}, sound{a} { println("ctor: {}", name); } ~Animal() { println("dtor: {}", name); } }; This class has two members, string objects for the name and sound of the Animal . We also have print() statements for the constructor and destructor.
Animal , but share only its name and sound : auto make_animal(const string& n, const string& s) { auto ap = make_shared<Animal>(n, s); auto np = shared_ptr<string>(ap, &ap->name); auto sp = shared_ptr<string>(ap, &ap->sound); return tuple(np, sp); } This function creates a shared_ptr with an Animal object, constructed with a name and a sound . We then create aliased shared_ptr objects for the name and sound . When we return the name and sound pointers, the Animal pointer goes out of scope. But it is not deleted because the aliased pointers keep the use count from reaching zero.
main() function we call make_animal() and inspect the results: int main() { auto [name, sound] = make_animal("Velociraptor", "Grrrr!"); println("The {} says {}", *name, *sound); println("Use count: name {}, sound {}", name.use_count(), sound.use_count()); } Output:
ctor: Velociraptor The Velociraptor says Grrrr! Use count: name 2, sound 2 dtor: Velociraptor We can see that the aliased pointers each show a use_count of 2 . When the make_animal() function creates the aliased pointers, they each increase the use count of the Animal pointer. When the function ends, the Animal pointer goes out of scope, leaving its use count at 2 , which is reflected in the aliased pointers. The aliased pointers go out of scope at the end of main() , which allows the Animal object to be destroyed.
The aliased shared pointer seems a bit abstract, but it's simpler than it appears.
A shared pointer uses a control block to manage its resources. One control block is associated with one managed object and is shared among the pointers that share that object. The control block generally contains:
shared_ptr objects that own the managed object (this is the use count ) weak_ptr objects that refer to the managed object In the case of an aliased shared pointer, the control block includes the pointer to the aliased object . Everything else remains the same.
Aliased shared pointers participate in the use count, just like non-aliased shared pointers, preventing the managed object from being destroyed until the use count reaches zero. The deleter is not changed, so it destroys the managed object.
It is possible to use any pointer to construct an aliased pointer. Usually, the pointer refers to a member within the aliased object. If the aliased pointer does not refer to part of the managed object, you will need to manage its construction and destruction separately.
The random library provides a selection of random number generators, each with different strategies and properties. In this recipe, we examine a function to compare the different options by creating a histogram of their outputs.
In this recipe we compare the different random number generators provided by the C++ random library.
constexpr size_t n_samples {1000}; constexpr size_t n_partitions {10}; constexpr size_t n_max {50}; n_samples is the number of samples to examine. n_partitions is the number of partitions in which to display the samples, and n_max is the maximum size of a bar in the histogram (this will vary some due to rounding).
These numbers provide a reasonable display of the differences between the engines. Increasing the ratio of samples vs partitions tends to smooth out the curves and obscure the differences between the engines.
template <typename RNG> void histogram(const string_view& rng_name) { auto p_ratio = (double)RNG::max() / n_partitions; RNG rng{}; vector<size_t> v(n_partitions); for (size_t i{}; i < n_samples; ++i) { ++v[(size_t)(rng() / p_ratio)]; } auto max_el = std::max_element(v.begin(), v.end()); auto v_ratio = *max_el / n_max; if (v_ratio < 1) v_ratio = 1; println("engine: {}", rng_name); for (size_t i{}; i < n_partitions; ++i) { println("{:02}:{:*<{}}", i + 1, ' ', v[i] / v_ratio); } print("\n"); } This function stores a histogram of collected samples in a vector . It then displays the histogram as a series of asterisks on the console.
histogram() from main() like this: int main() { histogram<std::random_device>("random_device"); histogram<std::default_random_engine> ("default_random_engine"); histogram<std::minstd_rand0>("minstd_rand0"); histogram<std::minstd_rand>("minstd_rand"); histogram<std::mt19937>("mt19937"); histogram<std::mt19937_64>("mt19937_64"); histogram<std::ranlux24_base>("ranlux24_base"); histogram<std::ranlux48_base>("ranlux48_base"); histogram<std::ranlux24>("ranlux24"); histogram<std::ranlux48>("ranlux48"); histogram<std::knuth_b>("knuth_b"); } Output:
Figure 8.1: Screenshot of output from first two random number engines
This screenshot shows histograms of the first two random number engines. Your output will vary.
If we raise the value of n_samples to 100,000 you'll see the variance between engines becomes more difficult to discern:
Figure 8.2: Screenshot of output with 100,000 samples
Each of the random number engines has a functor interface that returns the next random number in the sequence:
result_type operator()(); The functor returns a random value evenly distributed between the min() and max() values. Each of the random number engines has this interface in common.
The histogram() function takes advantage of this uniformity by using the class of the random number engine in a template:
template <typename RNG> (RNG is a common abbreviation for Random Number Generator . The library documentation refers to these classes as engines , which is synonymous for our purposes.)
We instantiate an object with the RNG class and create a histogram in a vector:
RNG rng{}; vector<size_t> v(n_partitions); for(size_t i{}; i < n_samples; ++i) { ++v[rng() / p_ratio]; } We can easily compare the results of the various random number engines with this technique.
Each of the random number engines in the library has different methodologies and characteristics. When you run the histogram multiple times, you'll notice that most of the engines have the same distribution each time they're run. That's because they are deterministic , that is, they generate the same sequence of numbers each time. The std::random_device is non-deterministic on most systems. You can use it to seed one of the other engines if you need more variation. It is also common to seed an RNG with the current date and time.
The std::default_random_engine is a suitable choice for most purposes.
The C++ Standard Library provides a selection of random number distribution generators, each with its own properties. In this recipe we examine a function to compare the different options by creating a histogram of their outputs.
Like the random number engines, the distribution generators have some common interface elements. Unlike the random number engines, the distribution generators have a variety of properties to set. We can create a template function to print a histogram of the various distributions, but their initializations vary significantly.
constexpr size_t n_samples {10 * 1000}; constexpr size_t n_max {50}; The n_samples constant is the number of samples to generate for each histogram. In this case, 10,000.
The n_max constant is used as a divisor while generating the histograms.
void dist_histogram(auto distro, const string_view& dist_name) { std::default_random_engine rng{}; map<long, size_t> m; for (size_t i{}; i < n_samples; ++i) ++m[(long)distro(rng)]; auto max_elm_it = max_element(m.begin(), m.end(), [](const auto& a, const auto& b) { return a.second < b.second; } ); size_t max_elm = max_elm_it->second; size_t max_div = std::max(max_elm / n_max, size_t(1)); println ("{}:", dist_name); for (const auto [randval, count] : m) { if (count < max_elm / n_max) continue; println("{:3}:{:*<{}}", randval, ' ', count / max_div); } } The dist_histogram() function uses a map to store the histogram. It then displays the histogram as a series of asterisks on the console.
dist_histogram() from main() like this: int main() { dist_histogram( std::uniform_int_distribution<int>{0, 9}, "uniform_int_distribution"); dist_histogram( std::normal_distribution<double>{0.0, 2.0}, "normal_distribution"); ... Calling the dist_histogram() function is more complex than it was for the random number generators. Each random distribution class has a different set of parameters, according to its algorithm.
For the full list, see the distribution.cpp file in the Github archive.
Partial output:
Figure 8.3: Screenshot of random distribution histograms
Each of the distribution algorithms produces very different output. You will want to experiment with the different options for each random distribution generator.
Each of the distribution generators has a functor that returns the next value in the random distribution:
result_type operator()( Generator& g ); The functor takes a random number generator (RNG) object as an argument:
std::default_random_engine rng{}; map<long, size_t> m; for (size_t i{}; i < n_samples; ++i) ++m[(long)distro(rng)]; For our purposes, we're using the std::default_random_engine for our RNG.
As with the RNG histogram, this is a useful tool to visualize the various distribution algorithms available in the random library. You will want to experiment with the various parameters available for each algorithm.
Scan the QR code (or go to ). Search for this book by name, confirm the edition, and then follow the steps on the page.
Note: Keep your invoice handy. Purchases made directly from Packt don't require an invoice.