Today we’ll continue to explore the C++ Standard Library by delving into its utility classes and functions. These extremely common tools provide us with basics like std::tuple whose C# equivalent is so essential it’s built right into the language.

Table of Contents

Exception

Let’s start by looking at how the Standard Library codifies error-handling. There are a lot of kinds of errors from a lot of sources that can be dealt with in a lot of ways, so it’s no surprise that the Standard Library provides a lot of different approaches to error-handling.

To begin, let’s look at the <exception> header. As in C#, exceptions are the primary error-handling approach in C++. As C# has System.Exception as the base class of all exceptions, C++ has std::exception as the base class of all Standard Library exceptions. We’re free to throw anything, not just std::exception, but the Standard Library only throws this type and and many C++ codebases do the same.

#include <exception>
 
// Derive our own exception type
struct MyException : public std::exception
{
    const char* msg;
 
    MyException(const char* msg)
        : msg(msg)
    {
    }
 
    virtual const char* what() const noexcept override
    {
        return msg;
    }
};
 
try
{
    throw MyException{ "boom" };
}
catch (const std::exception& ex)
{
    DebugLog(ex.what()); // boom
}

This shows the standard usage pattern of C++ exceptions. When throwing, we throw an object as opposed to a pointer to an object allocated with the new operator. When catching, we catch by const lvalue reference. This avoids making a copy of the exception object and avoids accidentally changing the exception in the catch block.

All std::exception objects have a virtual what() function returning an error message just as System.Exception has a Message property in C#. Both C++ and C# have many types derived their base exception classes to provide additional detail about the error. This is done via the type system as well as the possibility of adding additional members to the derived types. We’ll see some of those later in this article.

C++ provides a way to capture these std::exception objects so we can deal with them later. For example, we might want to catch exceptions on one thread and re-throw them on another to provide thread-safety.

#include <exception>
 
// A class that acts like a pointer to a captured exception
std::exception_ptr capturedEx;
 
try
{
    // Do something that throws
    throw MyException{ "boom" };
}
// Catch anything
catch (...)
{
    // Capture the current exception
    capturedEx = std::current_exception();
}
 
// Later...
try
{
    // Check if an exception was captured
    if (capturedEx)
    {
        // If so, re-throw it
        std::rethrow_exception(capturedEx);
    }
}
catch (const std::exception& ex)
{
    DebugLog(ex.what()); // boom
}

There’s also a way to nest exceptions within each other:

#include <exception>
 
// Recursively print an exception and all its nested exceptions
void PrintNestedExceptions(const std::exception& ex)
{
    DebugLog(ex.what());
 
    try
    {
        // If ex is a std::nested_exception, re-throw its nested std::exception
        // Otherwise do nothing
        std::rethrow_if_nested(ex);
    }
    catch (const std::exception& nestedEx)
    {
        // Recurse to print the nested exception (and its nested exceptions)
        PrintNestedExceptions(nestedEx);
    }
}
 
// Function that throws an exception
FILE* OpenFile(const char* path)
{
    FILE* handle = fopen(path, "r");
    if (!handle)
    {
        throw MyException{ "Error opening file" };
    }
    return handle;
}
 
// Function that calls a function that throws an exception
// It throws an exception with the caught exception nested
void PrintFirstByte(const char* path)
{
    try
    {
        // Call a function that throws an exception
        FILE* f = OpenFile(path);
 
        DebugLog("First byte:", fgetc(f));
        fclose(f);
    }
    // Catch OpenFile exceptions
    catch (...)
    {
        // Throw an exception with the caught exception nested in it
        std::throw_with_nested(MyException{ "Failed to read file" });
    }
}
 
try
{
    // Call a function that throws a std::nested_exception
    PrintFirstByte("/path/to/missing/file");
}
// Catch all std::exception objects
// Includes the derived std::nested_exception type
catch (const std::exception& ex)
{
    PrintNestedExceptions(ex);
}

We have ways to customize what happens when std::terminate or std::unexpected are called. The language says that std::terminate is called for a variety of reasons including a noexcept function throwing an exception or an exception that’s never caught. The std::unexpected function was removed in C++17, but it was previously called when a dynamic exception specification (throw(MyException)) was violated. Dynamic exception specifications were also removed in C++17.

#include <exception>
 
// Set a lambda to be called when std::terminate is called
std::set_terminate([]() { DebugLog("std::terminate called"); });
 
// Throw an exception and never catch it
// This causes std::terminate to be called
// The lambda is then called
throw MyException{ "boom" };

And finally, we can use std::uncaught_exceptions to check how many exceptions have been thrown that haven’t yet been caught by a catch block. A singular version, std::uncaught_exception, was available until C++20 when it was removed. Multiple exceptions can be uncaught when destructors throw exceptions themselves and the plural std::uncaught_exceptions allows us to check for that:

#include <exception>
 
struct Second
{
    ~Second()
    {
        DebugLog("Second", std::uncaught_exceptions());
    }
};
 
struct First
{
    ~First()
    {
        DebugLog("First before", std::uncaught_exceptions());
 
        try
        {
            Second sec;
            throw std::runtime_error{ "boom" };
        } // Note: sec destructor called
        catch (const std::exception& e)
        {
            DebugLog("First caught", e.what());
        }
 
        DebugLog("First after", std::uncaught_exceptions());
    }
};
 
void Foo()
{
    try
    {
        First fir;
        throw std::runtime_error{ "boom" };
    } // Note: fir destructor called
    catch (const std::exception& e)
    {
        DebugLog("Foo", e.what()); // boom
    }
 
    First fir2;
} // Note: fir2 destructor called

This prints the following:

First before 1
Second 2
First caught boom
First after 1
Foo boom
First before 0
Second 1
First caught boom
First after 0
Standard Exceptions

Now let’s look at some of the classes that derive from std::exception to describe particular categories of errors. These are available in <stdexcept>:

#include <stdexcept>
 
int GetLastElement(int* array, int length)
{
    if (array == nullptr || length <= 0)
    {
        // C# approximation: ArgumentException
        throw std::invalid_argument{ "Invalid array" };
    }
 
    return array[length - 1];
}
 
float Sqrt(float val)
{
    if (val < 0)
    {
        // C# approximation: ArgumentNullException, DivideByZeroException, etc.
        throw std::domain_error{ "Value must be non-negative" };
    }
 
    return std::sqrt(val);
}
 
template <typename T, int N>
void WriteToBuffer(const T& obj, char buf[N])
{
    if (sizeof(T) > N)
    {
        // C# approximation: ArgumentException
        throw std::length_error{ "Object is too big for the buffer" };
    }
 
    std::memcpy(buf, &obj, sizeof(T));
}
 
void CheckedIncrement(uint32_t& x)
{
    if (x == 0xffffffff)
    {
        // C# approximation: ArgumentException
        throw std::out_of_range{ "Overflow" };
    }
    x++;
}
 
int BinarySearch(int* array, int length)
{
    #if NDEBUG
        for (int i = 1; i < length; ++i)
        {
            if (array[i - 1] > array[i])
            {
                // C# approximation: ArgumentException
                // Note: base class of all of the above
                throw std::logic_error{ "Array isn't sorted" };
            }
        }
    #endif
 
    // ...implementation...
}

The Standard Library itself throws these exception types. We’re also free to throw them in our own code and it’s common to do so.

System Error

Next up, let’s look at the <system_error> header. As we saw in the C Standard Library, there are a lot of “error codes” exposed to us via mechanisms like return values and the global errno macro. These error codes are platform-specific. The C++ Standard Library includes a platform-independent alternative in a pair of types: std::error_condition and std::error_category.

#include <system_error>
 
// Get the "generic" error category
const std::error_category& category = std::generic_category();
DebugLog(category.name()); // Maybe "generic"
 
// Build an error_condition representing the "no space on device" code
std::error_condition condition = category.default_error_condition(ENOSPC);
DebugLog(condition.value() == ENOSPC); // true
DebugLog(condition.message()); // Maybe "no space on device"
 
// There are other categories
const std::error_category& sysCat = std::system_category();
DebugLog(sysCat.name()); // Maybe "system"

We use these classes to convert platform-specific error codes to platform-independent error codes and then take action on them. We get the added bonus of stronger typing since these classes aren’t simply an int and are therefore less likely to be misused.

The reverse is also supported. We have the value member function above to get platform-specific error codes back out of platform-independent std::error_condition objects. We also have std:errc which is an enum class that allows us to avoid macros like ENOSPC and strongly-type error codes as opposed to simple int values. These are often attached to a std::system_exception type derived from std::exception:

#include <system_error>
 
try
{
    // Handle a platform-specific error: ENOSPC
    throw std::system_error{
        ENOSPC, std::generic_category(), "Disk is full" };
}
catch (const std::system_error& e)
{
    // Platform-specific error (ENOSPC) converted to a std::errc enumerator
    DebugLog(e.code() == std::errc::no_space_on_device); // true
 
    DebugLog(e.what()); // Maybe "Disk is full: no space on device"
}
Utility

Moving on from error-handling, let’s look at some truly generic utility functions provied by the <utility> header:

#include <utility>
 
// Swap two values
int x = 2;
int y = 4;
std::swap(x, y);
DebugLog(x, y); // 4, 2
 
// Set a value and return the old value
int old = std::exchange(x, 6);
DebugLog(x, old); // 6, 4
 
// Get a const version of anything
const int& c = std::as_const(x);
DebugLog(c); // 6
 
// Compare integers without conversion
DebugLog(-1 > 1U); // true!
DebugLog(std::cmp_greater(-1, 1U)); // false
 
// Check if an integer fits in an integer type
DebugLog(std::in_range<uint8_t>(200)); // true
DebugLog(std::in_range<uint8_t>(500)); // false
 
// Cast to an rvalue reference
int&& rvr = std::move(x);
DebugLog(x, rvr); // 6, 6
 
// Forward a value as an lvalue or rvalue reference
int f1 = std::forward<int&>(x);
int f2 = std::forward<int&&>(x);

There are also a couple of utility classes available. First, we have std::integer_sequence to deal with parameter packs of integers:

#include <utility>
 
// Variadic function template taking a std::integer_sequence
template<typename T, T... vals>
void PrintInts(std::integer_sequence<T, vals...> is)
{
    // Provides the number of integers
    DebugLog(is.size());
 
    // Use the parameter pack to get the values
    DebugLog(vals...);
}
 
// Prints "3" then "123, 456, 789"
PrintInts(std::integer_sequence<int32_t, 123, 456, 789>{});

Next we have std::pair which is a struct holding two values. This is similar to KeyValuePair in C#:

#include <utility>
 
// Make a struct with an int and a float as non-static data members
std::pair<int, float> p{ 123, 3.14f };
 
// Get them in two ways
DebugLog(p.first, p.second); // 123, 3.14
DebugLog(std::get<0>(p), std::get<1>(p)); // 123, 3.14
 
// We can also use std::make_pair to use type deduction to avoid
// specifying the types ourselves
p = std::make_pair(123, 3.14f);
DebugLog(p.first, p.second); // 123, 3.14
 
// make_pair is less necessary in C++17 with template argument deduction
std::pair p2{ 456, 2.2f };
DebugLog(p2.first, p2.second); // 456, 2.2
 
// std::swap works with std::pair
std::swap(p, p2);
DebugLog(p.first, p.second); // 456, 2.2
DebugLog(p2.first, p2.second); // 123, 3.14
Tuple

std::pair has largely been eclipsed by the more generic std::tuple in <tuple>. It can hold any number of data members, not just two. This is like the ValueTuple family of classes in C#: ValueTuple<T>, ValueTuple<T1, T2>, ValueTuple<T1, T2, T3>, etc. There’s only one class template in C++ since variadic templates are supported, so truly any number of data members may be added to a std::tuple:

#include <tuple>
 
// Make a struct with an int and a float as non-static data members
std::tuple<int, float> t{ 123, 3.14f };
 
// Get them, but only with std::get since there are no names
DebugLog(std::get<0>(t), std::get<1>(t)); // 123, 3.14
 
// We can also use std::make_tuple to use type deduction to avoid
// specifying the types ourselves
t = std::make_tuple(123, 3.14f);
DebugLog(std::get<0>(t), std::get<1>(t)); // 123, 3.14
 
// make_tuple is less necessary in C++17 with template argument deduction
std::tuple t2{ 456, 2.2f };
DebugLog(std::get<0>(t2), std::get<1>(t2)); // 456, 2.2
 
// std::swap works with std::tuple
std::swap(t, t2);
DebugLog(std::get<0>(t), std::get<1>(t)); // 456, 2.2
DebugLog(std::get<0>(t2), std::get<1>(t2)); // 123, 3.14

std::tuple has some extended functionality beyond what’s provided for std::pair:

#include <tuple>
 
std::tuple t{ 123, 3.14f, "hello" };
 
// Get the number of elements in the tuple at compile time
constexpr std::size_t size = std::tuple_size_v<decltype(t)>;
DebugLog(size); // 3
 
// Get the type of an element of the tuple
std::tuple_element_t<1, decltype(t)> second = std::get<1>(t);
DebugLog(second); // 3.14
 
// Create a tuple of lvalue references to variables
int i = 456;
float f = 2.2f;
std::tuple tied = std::tie(i, f);
i = 100;
f = 200;
DebugLog(std::get<0>(tied), std::get<1>(tied)); // 100, 200
 
// Convert from std::pair to std::tuple
std::pair p{ 2, 4 };
std::tuple t2{ 0, 0 };
t2 = p;
DebugLog(std::get<0>(t2), std::get<1>(t2)); // 2, 4
 
// Concatenate tuples
std::tuple<
    // Types from t
    int, float, const char*,
    // Types from tied
    int, float,
    // Types from t2
    int, int
> cat = std::tuple_cat(t, tied, t2);
DebugLog(
    std::get<0>(cat),
    std::get<1>(cat),
    std::get<2>(cat),
    std::get<3>(cat),
    std::get<4>(cat),
    std::get<5>(cat),
    std::get<6>(cat)); // 123, 3.14, hello, 100, 200, 2, 4
 
struct IntVector
{
    int X;
    int Y;
};
 
// Instantiate a class by passing the data members of a tuple to a
// constructor of that class
IntVector iv = std::make_from_tuple<IntVector>(t2);
DebugLog(iv.X, iv.Y); // 2, 4
 
// Make a function call, passing the data members of a tuple as arguments
DebugLog(std::apply([](int a, int b) { return a + b; }, t2)); // 6

Unlike C#, there’s no way to name the data members of a C++ std::tuple. We simply refer to them by index similar to using the default Item1, Item2, etc. names in C#.

Variant

The <variant> header provides std::variant, which is essentially a generic tagged union. It holds one of many types and is as big as the largest of them. This type is very useful to pass, return, or hold one of many types. There’s no similar type in C#, but we can create our own family of them.

#include <variant>
 
// Make a variant that holds either an int32_t or a double
// Start off holding an int32_t
std::variant<int32_t, double> v{ 123 };
DebugLog(std::get<int32_t>(v)); // 123
DebugLog(v.index()); // 0
 
// Switch to holding a double
v = 3.14;
DebugLog(std::get<double>(v)); // 3.14
DebugLog(v.index()); // 1
 
// Trying to get a type that's not current throws an exception
DebugLog(std::get<int>(v)); // throws std::bad_variant_access
 
// Check the type before getting it
if (std::holds_alternative<int32_t>(v))
{
    DebugLog("int32_t", std::get<int32_t>(v)); // not printed
}
else
{
    DebugLog("double", std::get<double>(v)); // double 3.14
}
 
// Get an int32_t pointer if that's the current type
// If it's not, get nullptr
if (int32_t* pVal = std::get_if<int32_t>(&v))
{
    DebugLog(*pVal); // not printed
}
else
{
    DebugLog("not an int"); // printed
}
 
// These helpers are common boilerplate to use lambdas with std::visit
// They're usually stashed away in some "utilities" header file
template<class... TFuncs> struct overloaded : TFuncs...
{
    using TFuncs::operator()...;
};
template<class... TFuncs> overloaded(TFuncs...) -> overloaded<TFuncs...>;
 
// Call the appropriate lambda for the variant's current type
std::visit(overloaded {
    [](double val) { DebugLog("double", val); }, // double 3.14
    [](int32_t val) { DebugLog("int32_t", val); } // not printed
    }, v);
 
// A class without a default constructor
struct IntWrapper
{
    int Val;
 
    IntWrapper(int val)
        : Val(val)
    {
    }
};
 
// Compiler error: first type needs to be default constructible
std::variant<IntWrapper, float> v2;
 
// No compiler error: std::monostate is default constructible
// It's just a placeholder to work around this issue
std::variant<std::monostate, IntWrapper, float> v2;
 
// We can get a monostate, but it has no members so there's no reason to
std::monostate m = std::get<std::monostate>(v2);
Optional

Similar to std::variant holding one of many types, std::optional holds either a value or the absence of a value. It’s similar to Nullable<T>/T? in C# as well as Optional.

#include <optional>
 
// Create an optional with a value
std::optional<float> f{ 3.14f };
 
// Dereference it like a pointer to get its value
DebugLog(*f); // 3.14
 
// By default it has no value
std::optional<float> f2;
 
// Dereferencing without a value is undefined behavior
DebugLog(*f2); // Could be anything!
 
// Manually check for a value
if (f2.has_value())
{
    DebugLog(*f2); // not printed
}
else
{
    DebugLog("no value"); // gets printed
}
 
// Can also check by converting to bool
if (f2)
{
    DebugLog(*f2); // not printed
}
else
{
    DebugLog("no value"); // gets printed
}
 
// The value member function throws an exception if there's no value
DebugLog(f2.value()); // Throws std::bad_optional_access
 
// Get the value or a default
DebugLog(f2.value_or(0)); // 0
 
// Assign a value
f2 = 2.2f;
DebugLog(*f2); // 2.2
 
// Clear a value
f2.reset();
DebugLog(f2.has_value()); // false
 
// The nullopt constant indicates "no option"
f = std::nullopt;
DebugLog(f.has_value()); // false
Any

Similar to C#’s base System.Object/object type, C++ has std::any in the <any> header. This is a container for any type of object or, similar to null, no object at all.

#include <any>
 
// Create an empty std::any
std::any a;
 
// Check whether it has a value or is empty
if (a.has_value())
{
    DebugLog("has value"); // not printed
}
else
{
    DebugLog("empty"); // gets printed
}
 
// Set its value
a = 3.14f;
 
// Check the type
DebugLog(a.type() == typeid(float)); // true
 
// Get the value
// Note: not a real cast. Just a function with "cast" in the name.
DebugLog(std::any_cast<float>(a)); // 3.14
 
// Getting the wrong type throws an exception
try
{
    DebugLog(std::any_cast<int32_t>(a));
}
catch (const std::bad_any_cast& ex)
{
    DebugLog(ex.what()); // Maybe "Bad any_cast"
}
 
// Destroy the value and go back to being empty
a.reset();
DebugLog(a.has_value()); // false
 
// Another way to create a std::any
a = std::make_any<int32_t>(123);
DebugLog(std::any_cast<int32_t>(a)); // 123
Bit Set

C# has BitArray to represent an array of bits. The C++ equivalent is std::bitset which is a class templated on the number of bits it holds:

#include <bitset>
 
// Holds three bits that are all zero
std::bitset<3> zeroes;
 
// Indexing gives us bool values
DebugLog(zeroes[0], zeroes[1], zeroes[2]); // false, false, false
 
// Get a bit, but throw an exception if out of bounds
DebugLog(zeroes.test(1)); // false
//DebugLog(zeroes.test(3)); // throws std::out_of_range
 
// Manually bounds-check against the number of bits
if (3 < zeroes.size())
{
    DebugLog(zeroes[3]); // not printed
}
else
{
    DebugLog("out of bounds"); // gets printed
}
 
// Convert the bits of an unsigned long to a bitset
std::bitset<3> bits{ 0b101ul };
DebugLog(bits[0], bits[1], bits[2]); // true, false, true
 
// Compare bitsets
DebugLog(zeroes == bits); // false
 
// Check all the bits against 1
DebugLog(bits.all()); // false
DebugLog(bits.any()); // true
DebugLog(bits.none()); // false
DebugLog(bits.count()); // 2
 
// Set a bit
bits.set(0, false);
DebugLog(bits[0], bits[1], bits[2]); // false, false, true
 
// Set all bits to true or false
bits.set();
DebugLog(bits[0], bits[1], bits[2]); // true, true, true
bits.reset();
DebugLog(bits[0], bits[1], bits[2]); // false, false, false
 
// Perform bit operations on the set
bits |= 0b010;
DebugLog(bits[0], bits[1], bits[2]); // false, true, false
bits >>= 1;
DebugLog(bits[0], bits[1], bits[2]); // true, false, false
 
// Get bits as an integer
unsigned long ul = bits.to_ulong();
DebugLog(ul); // 1
 
// Bits are represented in a compact manner
std::bitset<1024> kb;
DebugLog(sizeof(kb)); // 128
Functional

Finally for today, <functional> contains function-related utilities. Some of these are class templates that have an operator() so they can be called like functions. These were more useful before lambdas were introduced to the language, but still commonly seen as a named shorthand alternative to them:

#include <functional>
 
// An object to perform +
std::plus<int32_t> add;
DebugLog(add(2, 3)); // 5
 
// An object to perform ==
std::equal_to<int32_t> equal;
DebugLog(equal(2, 2)); // true
 
// An object to perform ||
std::logical_and<int32_t> la;
DebugLog(la(1, 0)); // false
 
// An object to perform |
std::bit_and<int32_t> ba;
DebugLog(ba(0b110, 0b011)); // 2 (0b010)
 
// An object to perform the negation of another object
auto ne = std::not_fn(equal);
DebugLog(ne(2, 3)); // true
 
// Some class with a member function
struct Adder
{
    int32_t AddOne(int32_t val)
    {
        return val + 1;
    }
};
 
// An object to call a member function
auto addOne = std::mem_fn(&Adder::AddOne);
Adder adder;
DebugLog(addOne(adder, 2)); // 3

A std::function class template is provided to encapsulate any kind of callable object including lambdas, free functions, and function objects. This is similar to delegates like Action or Func in C#, except that it represents only one function:

#include <functional>
 
// Create a std::function that calls a lambda
std::function<int32_t(int32_t, int32_t)> add{
    [](int32_t a, int32_t b) {return a + b; } };
DebugLog(add(2, 3)); // 5
 
// Create a std::function that calls a free function
int32_t Add(int32_t a, int32_t b)
{
    return a + b;
}
std::function<int32_t(int32_t, int32_t)> add2{Add};
DebugLog(add2(2, 3)); // 5
 
// Create a std::function that calls operator() on a class object
struct Adder
{
    int32_t operator()(int32_t a, int32_t b)
    {
        return a + b;
    }
};
std::function<int32_t(int32_t, int32_t)> add3{ Adder{} };
DebugLog(add3(2, 3)); // 5

Similarly, std::bind also creates a callable object by “binding” one or more values and placeholder parameters to it:

#include <functional>
 
// Create an object that calls a lambda
auto add = std::bind(
    // Lambda to call
    [](int32_t a, int32_t b) { return a + b; },
    // Placeholders for parameters
    std::placeholders::_1,
    std::placeholders::_2);
DebugLog(add(2, 3)); // 5
 
// Create an object that calls a free function
int32_t Add(int32_t a, int32_t b)
{
    return a + b;
}
auto add2 = std::bind(
    // Free function to call
    Add,
    // Placeholders for parameters
    std::placeholders::_1,
    std::placeholders::_2);
DebugLog(add2(2, 3)); // 5
 
// Create an object that calls a member function
struct Adder
{
    int32_t Add(int32_t a, int32_t b)
    {
        return a + b;
    }
};
Adder adder;
auto add3 = std::bind(
    // Member function to call
    &Adder::Add,
    // Object to call it on
    &adder,
    // Placeholders for parameters
    std::placeholders::_1,
    std::placeholders::_2);
DebugLog(add3(2, 3)); // 5

In C++20, std::bind_front is available as a simpler alternative. It doesn’t support more complex options like out-of-order placeholders:

// Create an object that calls a lambda
auto add = std::bind_front(
    [](int32_t a, int32_t b) { return a + b; });
DebugLog(add(2, 3)); // 5
 
// Create an object that calls a free function
auto add2 = std::bind_front(Add);
DebugLog(add2(2, 3)); // 5
 
// Create an object that calls a member function
Adder adder;
auto add3 = std::bind_front(&Adder::Add, &adder);
DebugLog(add3(2, 3)); // 5

And finally, std::reference_wrapper stores a reference in a normal class object:

#include <functional>
 
// An integer and a reference to it
int x = 123;
int& r = x;
 
// Use std::ref to get a reference to it
std::reference_wrapper<int> w = std::ref(r);
 
// Can copy the wrapper without changing the reference
std::reference_wrapper<int> w2 = w;
 
// Unwrap the references
DebugLog(w.get(), w2.get()); // 123, 123
 
// Modifying one modifies the other
int& r2 = w2.get();
r2 = 456;
DebugLog(w.get(), w2.get()); // 456, 456
 
// std::cref gets a constant reference
std::reference_wrapper<const int> cw = std::cref(x);
cw.get() = 1000; // Compiler error: cw.get() is "const int&"
Conclusion

The C++ Standard Library provides a lot of utility functions and types. Most of them are templates, which is why the Standard Library is often called the Standard Template Library or STL.

We have a wide variety of error-handling utilities including platform-independent error codes and an inheritance tree of standardized exceptions akin to System.Exception in C#.

There are many general-purpose types available, too. std::optional gives us the ability to indicate that a value may or may not be present which is especially useful as a return value of functions that may fail to have a usable result. std::variant implements any tagged union for us and is a useful alternative to traditional inheritance trees. std::any takes the place of System.Object in C# for times where a value really could be any kind of type.

We’ve even got a lot of function-related tools like std::function to abstract the specific kind of function, as C# delegates do. We have many “callable” struct types as associated tools such as std::bind and named types like std::plus.

As we continue through the Standard Library, we’ll keep seeing utilities like std::exception crop up again and again.