C++ For C# Developers: Part 40 – Utilities Library
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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.
#1 by Nikita Lisitsa on February 15th, 2021 ·
The
code (that appears several times through the article) won’t compile:
doesn’t have a constructor that accepts string literals. The usual exception type to use in this situation is
.
#2 by jackson on February 15th, 2021 ·
You’re right! It turns out this is a Visual Studio-specific extension that adds a constructor overload to take a message. I happened to be using that compiler to write the examples, so they compiled for me. I’ve updated the article to add an example of deriving a
MyException
fromstd::exception
and am now throwing objects of that type instead. Thanks for pointing this out!#3 by tupoman on March 29th, 2021 ·
typo: “mehanisms”
#4 by jackson on April 4th, 2021 ·
Fixed. Thanks for letting me know!