Reader small image

You're reading from  Modern C++ Programming Cookbook - Third Edition

Product typeBook
Published inFeb 2024
PublisherPackt
ISBN-139781835080542
Edition3rd Edition
Right arrow
Author (1)
Marius Bancila
Marius Bancila
author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila

Right arrow

Initializing a range

In the previous recipes, we explored the general standard algorithms for searching in a range and sorting a range. The algorithms library provides many other general algorithms, and among them are several that are intended for filling a range with values. In this recipe, you will learn what these algorithms are and how they should be used.

Getting ready

All the examples in this recipe use std::vector. However, like all the general algorithms, the ones we will see in this recipe take iterators to define the bounds of a range and can therefore be used with any standard container, arrays, or custom types representing a sequence that have forward iterators defined.Except for std::iota(), which is available in the <numeric> header, all the other algorithms are found in the <algorithm> header.

How to do it...

To assign values to a range, use any of the following standard algorithms:

  • std::fill() to assign a value to all the elements of a range; the range...

Using set operations on a range

The standard library provides several algorithms for set operations that enable us to do unions, intersections, or differences of sorted ranges. In this recipe, we will see what these algorithms are and how they work.

Getting ready

The algorithms for set operations work with iterators, which means they can be used for standard containers, arrays, or any custom type representing a sequence that has input iterators available. All the examples in this recipe will use std::vector.For all the examples in the next section, we will use the following ranges:

std::vector<int> v1{ 1, 2, 3, 4, 4, 5 };
std::vector<int> v2{ 2, 3, 3, 4, 6, 8 };
std::vector<int> v3;

In the following section, we will explore the use of the standard algorithm for set operations.

How to do it...

Use the following general algorithms for set operations:

  • std::set_union() to compute the union of two ranges into a third range:
std::set_union(v1.cbegin(), v1.cend(),
...

Using iterators to insert new elements into a container

When you’re working with containers, it is often useful to insert new elements at the beginning, end, or somewhere in the middle. There are algorithms, such as the ones we saw in the previous recipe, Using set operations on a range, that require an iterator to a range to insert into, but if you simply pass an iterator, such as the one returned by begin(), it will not insert but overwrite the elements of the container. Moreover, it’s not possible to insert at the end by using the iterator returned by end(). In order to perform such operations, the standard library provides a set of iterators and iterator adapters that enable these scenarios.

Getting ready

The iterators and adapters discussed in this recipe are available in the std namespace in the <iterator> header. If you include headers such as <algorithm>, you do not have to explicitly include <iterator>.

How to do it...

Use the following iterator...

Writing your own random-access iterator

In the first chapter, we saw how we can enable range-based for loops for custom types by implementing iterators, as well as free begin() and end() functions to return iterators to the first and one-past-the-last element of the custom range. You might have noticed that the minimal iterator implementation that we provided in that recipe does not meet the requirements for a standard iterator. This is because it cannot be copy constructible or assigned and cannot be incremented. In this recipe, we will build upon that example and show you how to create a random-access iterator that meets all requirements.

Getting ready

For this recipe, you should know the types of iterators the standard defines and how they are different. A good overview of their requirements is available at http://www.cplusplus.com/reference/iterator/.To exemplify how to write a random-access iterator, we will consider a variant of the dummy_array class used in the Enabling range...

Container access with non-member functions

Standard containers provide the begin() and end() member functions for retrieving iterators for the first and one-past-last elements of the container. There are actually four sets of these functions. Apart from begin()/end(), containers provide cbegin()/cend() to return constant iterators, rbegin()/rend() to return mutable reverse iterators, and crbegin()/crend() to return constant reverse iterators. In C++11/C++14, all these have non-member equivalents that work with standard containers, arrays, and any custom type that specializes them. In C++17, even more non-member functions have been added: std::data(), which returns a pointer to the block of memory containing the elements of the container; std::size(), which returns the size of a container or array; and std::empty(), which returns whether the given container is empty. These non-member functions are intended for generic code but can be used anywhere in your code. Moreover, in C++20, the...

Selecting the right standard container

The standard library contains a variety of containers for meeting multiple and various needs. There are sequence containers (in which elements are arranged in a certain position), container adapters (that provide a different interface for sequential containers), associative containers (in which the order is given by a key associated with an element), unordered associative containers (in which the elements do not follow a certain order). Selecting the right container for a given task is not always straight forward. This recipe will provide guidelines to help you decide which one to use for what purpose.

How to do it…

To decide which standard container you should use, consider the following guidelines:

  • Use std::vector as the default container, when no other specific requirements exist.
  • Use std::array when the length of a sequence is fixed and known at compile time.
  • Use std::deque if you frequently need to add or remove elements at the beginning...

Using std::any to store any value

C++ does not have a hierarchical type system like other languages (such as C# or Java) and, therefore, it can’t store multiple types of a value in a single variable like it is possible to with the type Object in .NET and Java or natively in JavaScript. Developers have long used void* for that purpose, but this only helps us store pointers to anything and is not type-safe. Depending on the end goal, alternatives can include templates or overloaded functions. However, C++17 has introduced a standard type-safe container, called std::any, that can hold a single value of any type.

Getting ready

std::any has been designed based on boost::any and is available in the <any> header. If you are familiar with boost::any and have used it in your code, you can migrate it seamlessly to std::any.

How to do it...

Use the following operations to work with std::any:

  • To store values, use the constructor or assign them directly...

Using std::optional to store optional values

Sometimes, it is useful to be able to store either a value or a null pointer if a specific value is not available. A typical example of such a case is the return value of a function that may fail to produce a return value, but this failure is not an error. For instance, think of a function that finds and returns values from a dictionary by specifying a key. Not finding a value is a probable case and, therefore, the function would either return a Boolean (or an integer value, if more error codes are necessary) and have a reference argument to hold the return value or return a pointer (raw or smart pointer). In C++17, std::optional is a better alternative to these solutions. The class template std::optional is a template container for storing a value that may or may not exist. In this recipe, we will see how to use this container and its typical use cases.

Getting ready

The class template std::optional<T> was designed based...

Chaining together computations that may or may not produce a value

In the previous recipe, we have seen how to use the std::optional class for storing a value that may or may not exist. Its use cases include optional parameters to functions and return values from functions that may fail to produce a result. When multiple such functions need to be chained together, the code can become cumbersome and verbose. For this reason, the C++23 standard has added several new methods to the std::optional class. They are referred to by the term monadic operations. These methods are transform(), and_then(), and or_else(). In this recipe, we’ll see what they are useful for.

In simple terms, in functional programming, a monad is a container that encapsulates some functionality on top of a value that it wraps. Such an example is std::optional in C++. On the other hand, a monadic operation is a function from a domain D into D itself. For instance, the identity function (a function...

Using std::variant as a type-safe union

In C++, a union type is a special class type that, at any point, holds a value of one of its data members. Unlike regular classes, unions cannot have base classes, nor can they be derived, and they cannot contain virtual functions (that would not make sense anyway). Unions are mostly used to define different representations of the same data. However, unions only work for types that are Plain Old Data (POD). If a union contains values of non-POD types, then these members require explicit construction with a placement new and explicit destruction, which is cumbersome and error-prone. In C++17, a type-safe union is available in the form of a standard library class template called std::variant. In this recipe, you will learn how to use it to model alternative values.

Getting ready

The std::variant type implements a type-safe discriminated union. Although discussing these in detail is beyond the scope of this recipe, we will introduce them...

Visiting a std::variant

std::variant is a new standard container that was added to C++17 based on the boost.variant library. A variant is a type-safe union that holds the value of one of its alternative types. Although, in the previous recipe, we have seen various operations with variants, the variants we used were rather simple, with POD types mostly, which is not the actual purpose for which std::variant was created. Variants are intended to be used for holding alternatives of similar non-polymorphic and non-POD types. In this recipe, we will see a more real-world example of using variants and will learn how to visit variants.

Getting ready

For this recipe, you should be familiar with the std::variant type. It is recommended that you first read the previous recipe, Using std::variant as a type-safe union.

To explain how variant visitation can be done, we will consider a variant for representing a media DVD. Let’s suppose we want to model a store or library that...

Using std::expected to return a value or an error

We often need to write a function that returns both some data and an indication of success or failure (either as a bool for the simplest case or an error code for more complex cases). Typically, this can be solved either by returning a status code and using a parameter passed by reference for returning the data or by returning the actual data but throwing exceptions in the case of failure. In recent times, the availability of std::optional and std::variant gave way to new solutions for this problem. However, the C++23 standard provides a new approach with the std::expected type, a sort of combination of the two types previously mentioned. Such a type is present in other programming languages, such as Result in Rust and Either in Haskell. In this recipe, we will learn how to use this new std::expected class.

Getting ready

In the examples shown in this recipe, we will use the data types defined here:

enum class Status
{
...

Using std::span for contiguous sequences of objects

In C++17, the std::string_view type was added to the standard library. This is an object that represents a view over a constant contiguous sequence of characters. The view is typically implemented with a pointer to the first element of the sequence and a length. Strings are one of the most used data types in any programming language. They have a non-owning view that does not allocate memory, avoids copies, and performs some operations faster than std::string, which is an important benefit. However, a string is just a special vector of characters with operations specific to text. Therefore, it makes sense to have a type that is a view of a contiguous sequence of objects, regardless of their type. This is what the std::span class template in C++20 represents. We could say that std::span is to std::vector and array types what std::string_view is to std::string.

Getting ready

The std::span class template is available in the header...

Using std::mdspan for multi-dimensional views of sequences of objects

In the previous recipe, Using std::span for contiguous sequences of objects, we learned about the C++20 class called std::span, which represents a view (a non-owning wrapper) over a contiguous sequence of elements. This is similar to the C++17 std::string_view class, which does the same but for a sequence of characters. Both of these are views of one-dimensional sequences. However, sometimes we need to work with multi-dimensional sequences. These could be implemented in many ways, such as C-like arrays (int[2][3][4]), pointer-of-pointers (int** or int***), arrays of arrays (or vectors of vectors, such as vector<vector<vector<int>>>). A different approach is to use a one-dimensional sequence of objects but define operations that present it as a logical multi-dimensional sequence. This is what the C++23 std::mdspan class does: it represents a non-owning view of a contiguous sequence of objects presented...

Registering a function to be called when a program exits normally

It is common that a program, upon exit, must clean up code to release resources, write something to a log, or do some other end operation. The standard library provides two utility functions that enable us to register functions to be called when a program terminates normally, either by returning from main() or through a call to std::exit() or std::quick_exit(). This is particularly useful for libraries that need to perform an action before the program is terminated, without relying on the user to explicitly call an end function. In this recipe, you will learn how to install exit handlers and how they work.

Getting ready

All the functions discussed in this recipe, exit(), quick_exit(), atexit(), and at_quick_exit(), are available in the namespace std in the header <cstdlib>.

How to do it...

To register functions to be called upon termination of a program, you should use the following:

    ...

Using type traits to query properties of types

Template metaprogramming is a powerful feature of the language that enables us to write and reuse generic code that works with all types. In practice, however, it is often necessary that generic code should work differently, or not at all, with different types, either through intent or for semantic correctness, performance, or other reasons. For example, you may want a generic algorithm to be implemented differently for POD and non-POD types or a function template to be instantiated only with integral types. C++11 provides a set of type traits to help with this.

Type traits are basically meta-types that provide information about other types. The type traits library contains a long list of traits for querying type properties (such as checking whether a type is an integral type or whether two types are the same), but also for performing type transformation (such as removing the const and volatile qualifiers or adding a pointer to a...

Writing your own type traits

In the previous recipe, we learned what type traits are, what traits the standard provides, and how they can be used for various purposes. In this recipe, we’ll go a step further and take a look at how to define our own custom traits.

Getting ready

In this recipe, we will learn how to solve the following problem: we have several classes that support serialization. Without getting into any details, let’s suppose some provide a “plain” serialization to a string (regardless of what that can mean), whereas others do it based on a specified encoding. The end goal is to create a single, uniform API for serializing the objects of any of these types. For this, we will consider the following two classes: foo, which provides a simple serialization, and bar, which provides serialization with encoding.

Let’s look at the code:

struct foo
{
  std::string serialize()
  {
    return "plain"s;
  }
};
struct...

Using std::conditional to choose between types

In the previous recipes, we looked at some of the features from the type support library, and type traits in particular. Related topics have been discussed in other parts of this book, such as using std::enable_if to hide function overloads in Chapter 4, Preprocessing and Compilation, and std::decay to remove const and volatile qualifiers when we discussed visiting variants, also in this chapter. Another type transformation feature worth discussing to a larger extent is std::conditional, which enables us to choose between two types at compile time, based on a compile-time Boolean expression. In this recipe, you will learn how it works and how to use it through several examples.

Getting ready

It is recommended that you first read the Using type traits to query properties of types recipe, earlier in this chapter.

How to do it...

The following is a list of examples that show you how to use std::conditional (and std::conditional_t...

Providing logging details with source_location

Debugging is an essential part of software development. No matter how simple or complex it is, no program works as intended from the first shot. Therefore, developers spend a considerable amount of time debugging their code, employing various tools and techniques from debuggers to messages printed to a console or a text file. Sometimes, we want to provide detailed information about the source of a message in a log, including the file, the line, and maybe the function name. Although this was possible with some standard macros, in C++20, a new utility type called std::source_location allows us to do it in a modern way. In this recipe, we will learn how.

How to do it…

To log information including the file name, line number, and function name, do the following:

  • Define a logging function with parameters for all information that you need to provide (such as message, severity, etc.).
  • Add one additional parameter...

Using the stacktrace library to print the call sequence

In the previous recipe, we saw how to use the C++20 std::source_location to provide information about a source location for logging, testing, and debugging purposes. Another mechanism for debugging is represented by asserts but these are not always enough because we often need to know the sequence of calls that led to a point of execution. This is called the stack trace. The C++23 standard contains a new library with diagnostics utilities. This allows us to print the stack trace. In this recipe, you will learn how to use these diagnostics utilities.

How to do it…

You can use the C++23 stacktrace library to:

  • Print the entire content of the stack trace:
    std::cout << std::stacktrace::current() << '\n';
    
  • Iterate over each frame in the stack trace and print it:
    for (auto const & frame : std::stacktrace::current())
    {
       std::cout << frame <<...
lock icon
The rest of the chapter is locked
You have been reading a chapter from
Modern C++ Programming Cookbook - Third Edition
Published in: Feb 2024Publisher: PacktISBN-13: 9781835080542
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila