Book Image

Advanced C++ Programming Cookbook

By : Dr. Rian Quinn
Book Image

Advanced C++ Programming Cookbook

By: Dr. Rian Quinn

Overview of this book

If you think you've mastered C++ and know everything it takes to write robust applications, you'll be in for a surprise. With this book, you'll gain comprehensive insights into C++, covering exclusive tips and interesting techniques to enhance your app development process. You'll kick off with the basic principles of library design and development, which will help you understand how to write reusable and maintainable code. You'll then discover the importance of exception safety, and how you can avoid unexpected errors or bugs in your code. The book will take you through the modern elements of C++, such as move semantics, type deductions, and coroutines. As you advance, you'll delve into template programming - the standard tool for most library developers looking to achieve high code reusability. You'll explore the STL and learn how to avoid common pitfalls while implementing templates. Later, you'll learn about the problems of multithreaded programming such as data races, deadlocks, and thread starvation. You'll also learn high-performance programming by using benchmarking tools and libraries. Finally, you'll discover advanced techniques for debugging and testing to ensure code reliability. By the end of this book, you'll have become an expert at C++ programming and will have gained the skills to solve complex development problems with ease.
Table of Contents (15 chapters)

Using the noexcept specifier

The noexcept specifier is used to tell the compiler whether a function may or may not throw a C++ exception. If a function is marked with the noexcept specifier, it is not allowed to throw an exception and, if it does, std::terminate() will be called when the exception is thrown. If the function doesn't have the noexcept specifier, exceptions can be thrown as normal.

In this recipe, we will explore how to use the noexcept specifier in your own code. This specifier is important because it is a contract between the API that you are creating and the user of the API. When the noexcept specifier is used, it tells the user of the API that they do not need to consider exceptions when using the API. It also tells the author that if they add the noexcept specifier to their API, they have to ensure that no exceptions are thrown, which, in some cases, requires the author to catch all possible exceptions and either handle them or call std::terminate() if the exception cannot be handled. Also, there are certain operations, such as std::move, where exceptions cannot be thrown without the fear of corruption as a move operation oftentimes cannot be safely reversed if an exception is thrown. Finally, with some compilers, adding noexcept to your APIs will reduce the overall size of the function, resulting in a smaller overall application.

Getting ready

Before beginning, please ensure that all of the technical requirements are met, including installing Ubuntu 18.04 or higher and running the following in a Terminal window:

> sudo apt-get install build-essential git cmake

This will ensure your operating system has the proper tools to compile and execute the examples in this recipe. Once this is complete, open a new Terminal. We will use this Terminal to download, compile, and run our examples.

How to do it...

To try this recipe, perform the following steps:

  1. From a new Terminal, run the following to download the source code:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter02
  1. To compile the source code, run the following:
> mkdir build && cd build
> cmake ..
> make recipe01_examples
  1. Once the source code is compiled, you can execute each example in this recipe by running the following commands:
> ./recipe01_example01
The answer is: 42

> ./recipe01_example02
terminate called after throwing an instance of 'std::runtime_error'
what(): The answer is: 42
Aborted

> ./recipe01_example03
The answer is: 42

> ./recipe01_example04
terminate called after throwing an instance of 'std::runtime_error'
what(): The answer is: 42
Aborted

> ./recipe01_example05
foo: 18446744069414584320
foo: T is too large

In the next section, we will step through each of these examples and explain what each example program does and how it relates to the lessons being taught in this recipe.

How it works...

First, let's briefly review how C++ exceptions are thrown and caught. In the following example, we will throw an exception from a function and then catch the exception in our main() function:

#include <iostream>
#include <stdexcept>

void foo()
{
throw std::runtime_error("The answer is: 42");
}

int main(void)
{
try {
foo();
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}

return 0;
}

As shown in the preceding example, we created a function called foo() that throws an exception. This function is called in our main() function inside a try/catch block, which is used to catch any exceptions that might be thrown by the code executed inside the try block, which in this case is the foo() function. When the exception is thrown by the foo() function, it is successfully caught and outputted to stdout.

All of this works because we did not add the noexcept specifier to the foo() function. By default, a function is allowed to throw an exception, just as we did in this example. In some cases, however, we do not want to allow exceptions to be thrown, depending on how we expect a function to execute. Specifically, how a function handles exceptions can be defined as the following (known as exception safety):

  • No-throw guarantee: The function cannot throw an exception, and if an exception is thrown internally, the exception must be caught and handled, including allocation failures.
  • Strong exception safety: The function can throw an exception, and if an exception is thrown, any state that was modified by the function is rolled back or undone with no side effects.
  • Basic exception safety: The function can throw an exception, and if an exception is thrown, any state that was modified by the function is rolled back or undone, but side effects are possible. It should be noted that these side effects do not include invariants, meaning the program is in a valid, non-corrupted state.
  • No exception safety: The function can throw an exception, and if an exception is thrown, the program could enter a corrupted state.

In general, if a function has a no-throw guarantee, it is labeled with noexcept; otherwise, it is not. An example of why exception safety is so important is with std::move. For example, suppose we have two instances of std::vector and we wish to move one vector into another. To perform the move, std::vector might move each element of the vector from one instance to the other. If the object is allowed to throw when it is moved, the vector could end up with an exception in the middle of the move (that is, half of the objects in the vector are moved successfully). When the exception occurs, std::vector would obviously attempt to undo the moves that it has already performed by moving these back to the original vector before returning the exception. The problem is, attempting to move the objects back would require std::move(), which could throw and exception again, resulting in a nested exception. In practice, moving one std::vector instance to another doesn't actually perform an object-by-object move, but resizing does, and, in this specific issue, the standard library requires the use of std::move_if_noexcept to handle this situation to provide exception safety, which falls back to a copy when the move constructor of an object is allowed to throw.

The noexcept specifier is used to overcome these types of issues by explicitly stating that the function is not allowed to throw an exception. This not only tells the user of the API that they can safely use the function without fear of an exception being thrown and potentially corrupting the execution of the program, but it also forces the author of the function to safely handle all possible exceptions or call std::terminate(). Although noexcept, depending on the compiler, also provides optimizations by reducing the overall size of the application when defined, its main use is to state the exception safety of a function such that other functions can reason about how a function will execute.

In the following example, we add the noexcept specifier to our foo() function defined earlier:

#include <iostream>
#include <stdexcept>

void foo() noexcept
{
throw std::runtime_error("The answer is: 42");
}

int main(void)
{
try {
foo();
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}

return 0;
}

When this example is compiled and executed, we get the following:

As shown in the preceding example, the noexcept specifier was added, which tells the compiler that foo() is not allowed to throw an exception. Since, however, the foo() function does throw an exception, when it is executed, std::terminate() is called. In fact, in this example, std::terminate() will always be called, which is something the compiler is able to detect and warn about.

Calling std::terminate() is obviously not the desired outcome of a program. In this specific case, since the author has labeled the function as noexcept, it is up to the author to handle all possible exceptions. This can be done as follows:

#include <iostream>
#include <stdexcept>

void foo() noexcept
{
try {
throw std::runtime_error("The answer is: 42");
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}
}

int main(void)
{
foo();
return 0;
}

As shown in the preceding example, the exception is wrapped in a try/catch block to ensure the exception is safely handled before the foo() function completes its execution. Also, in this example, only exceptions that originate from std::exception() are caught. This is the author's way of saying which types of exceptions can be safely handled. If, for example, an integer was thrown instead of std::exception(), std::terminate() would still be executed automatically since noexcept was added to the foo() function. In other words, as the author, you are only required to handle the exceptions that you can, in fact, safely handle. The rest will be sent to std::terminate() for you; just understand that, by doing this, you change the exception safety of the function. If you intend for a function to be defined with a no-throw guarantee, the function cannot throw an exception at all.

It should also be noted that if you mark a function as noexcept, you need to not only pay attention to exceptions that you throw but also to the functions that may throw themselves. In this case, std::cout is being used inside the foo() function, which means the author has to either knowingly ignore any exceptions that std::cout could throw, which would result in a call to std::terminate() (which is what we are doing here), or the author needs to identify which exceptions std::cout could throw and attempt to safely handle them, including exceptions such as std::bad_alloc.

The std::vector.at() function throws an std::out_of_range() exception if the provided index is out of bounds with respect to the vector. In this case, the author can catch this type of exception and return a default value, allowing the author to safely mark the function as noexcept.

The noexcept specifier is also capable of acting as a function, taking a Boolean expression, as in the following example:

#include <iostream>
#include <stdexcept>

void foo() noexcept(true)
{
throw std::runtime_error("The answer is: 42");
}

int main(void)
{
try {
foo();
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}

return 0;
}

This results in the following when executed:

As shown in the preceding example, the noexcept specifier was written as noexcept(true). If the expression evaluates to true, it is as if noexcept was provided. If the expression evaluates to false, it is as if the noexcept specifier was left out, allowing exceptions to be thrown. In the preceding example, the expression evaluates to true, which means that the function is not allowed to throw an exception, which results in std::terminate() being called when foo() throws an exception.

Let's look at a more complicated example to demonstrate how this can be used. In the following example, we will create a function called foo() that will shift an integer value by 32 bits and cast the result to a 64-bit integer. This example will be written using template metaprogramming, allowing us to use this function on any integer type:

#include <limits>
#include <iostream>
#include <stdexcept>

template<typename T>
uint64_t foo(T val) noexcept(sizeof(T) <= 4)
{
if constexpr(sizeof(T) <= 4) {
return static_cast<uint64_t>(val) << 32;
}

throw std::runtime_error("T is too large");
}

int main(void)
{
try {
uint32_t val1 = std::numeric_limits<uint32_t>::max();
std::cout << "foo: " << foo(val1) << '\n';

uint64_t val2 = std::numeric_limits<uint64_t>::max();
std::cout << "foo: " << foo(val2) << '\n';
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}

return 0;
}

This results in the following when executed:

As shown in the preceding example, the issue with the foo() function is that if the user provides a 64-bit integer, it cannot shift by 32 bits without generating an overflow. If the integer provided, however, is 32 bits or less, the foo() function is perfectly safe. To implement the foo() function, we used the noexcept specifier to state that the function is not allowed to throw an exception if the provided integer is 32 bits or less. If the provided integer is greater than 32 bits, an exception is allowed to throw, which, in this case, is an std::runtime_error() exception stating that the integer is too large to be safely shifted.