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 RAII

RAII is a programming principle that states that a resource is tied to the lifetime of the object that acquired the resource. RAII is a powerful feature of the C++ language that really helps to set C++ apart from C, helping to prevent resource leaks and general instability.

In this recipe, we will dive into how RAII works and how RAII can be used to ensure that C++ exceptions do not introduce resource leaks. RAII is a critical technology for any C++ application and should be used whenever possible.

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...

You need to perform the following steps to try the recipe:

  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 recipe03_examples
  1. Once the source code is compiled, you can execute each example in this recipe by running the following commands:
> ./recipe03_example01
The answer is: 42

> ./recipe03_example02
The answer is: 42

> ./recipe03_example03
The answer is not: 43

> ./recipe03_example04
The answer is: 42

> ./recipe03_example05
step 1: Collect answers
The answer is: 42

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...

To better understand how RAII works, we must first examine how a class in C++ works as C++ classes are used to implement RAII. Let's look at a simple example. C++ classes provide support for both constructors and destructors as follows:

#include <iostream>
#include <stdexcept>

class the_answer
{
public:
the_answer()
{
std::cout << "The answer is: ";
}

~the_answer()
{
std::cout << "42\n";
}
};

int main(void)
{
the_answer is;
return 0;
}

This results in the following when compiled and executed:

In the preceding example, we create a class with both a constructor and a destructor. When we create an instance of the class, the constructor is called, and, when the instance of the class loses scope, the class is destroyed. This is a simple C++ pattern that has been around since the initial versions of C++ were created by Bjarne Stroustrup. Under the hood, the compiler calls a construction function when the class is first instantiated, but, more importantly, the compiler has to inject code into the program that executes the destruction function when the instantiation of the class loses scope. The important thing to understand here is that this additional logic is inserted into the program automatically by the compiler for the programmer.

Before the introduction of the classes, the programmer had to add construction and destruction logic to the program manually, and, while construction is a fairly simple thing to get right, destruction is not. A classic example of this type of issue in C is storing a file handle. The programmer will add a call to an open() function to open the file handle and, when the file is done, will add a call to close() to close the file handle, forgetting to execute the close() function on all possible error cases that might crop up. This is inclusive of when the code is hundreds of lines long and someone new to the program adds another error case, forgetting also to call close() as needed.

RAII solves this issue by ensuring that, once the class loses scope, the resource that was acquired is released, no matter what the control-flow path was. Let's look at the following example:

#include <iostream>
#include <stdexcept>

class the_answer
{
public:

int *answer{};

the_answer() :
answer{new int}
{
*answer = 42;
}

~the_answer()
{
std::cout << "The answer is: " << *answer << '\n';
delete answer;
}
};

int main(void)
{
the_answer is;

if (*is.answer == 42) {
return 0;
}

return 1;
}

In this example, we allocate an integer and initialize it in the constructor of a class. The important thing to notice here is that we do not need to check for nullptr from the new operator. This is because the new operator will throw an exception if the memory allocation fails. If this occurs, not only will the rest of the constructor not be executed, but the object itself will not be constructed. This means if the constructor successfully executed, you know that the instance of the class is in a valid state and actually contains a resource that will be destroyed when the instance of the class loses scope

The destructor of the class then outputs to stdout and deletes the previously allocated memory. The important thing to understand here is that, no matter what control path the code takes, this resource will be released when the instance of the class loses scope. The programmer only needs to worry about the lifetime of the class.

This idea that the lifetime of the resource is directly tied to the lifetime of the object that allocated the resource is important as it solves a complicated issue for the control flow of a program in the presence of C++ exceptions. Let's look at the following example:

#include <iostream>
#include <stdexcept>

class the_answer
{
public:

int *answer{};

the_answer() :
answer{new int}
{
*answer = 43;
}

~the_answer()
{
std::cout << "The answer is not: " << *answer << '\n';
delete answer;
}
};

void foo()
{
the_answer is;

if (*is.answer == 42) {
return;
}

throw std::runtime_error("");
}

int main(void)
{
try {
foo();
}
catch(...)
{ }

return 0;
}

In this example, we create the same class as the previous example, but, in our foo() function, we throw an exception. The foo() function, however, doesn't need to catch this exception to ensure that the memory allocated is properly freed. Instead, the destructor handles this for us. In C++, many functions might throw and, without RAII, every single function that could throw would need to be wrapped in a try/catch block to ensure that any resources that were allocated are properly freed. We, in fact, see this pattern a lot in C code, especially in kernel-level programming where goto statements are used to ensure that, within a function, if an error occurs, the function can properly unwind itself to release any resources might have previously been acquired. This result is a nest of code dedicated to checking the result of every function call within the program and the logic needed to properly handle the error.

With this type of programming model, it's no wonder that resource leaks are so common in C. RAII combined with C++ exceptions remove the need for this error-prone logic, resulting in code that is less likely to leak resources.

How RAII is handled in the presence of C++ exceptions is outside the scope of this book as it requires a deeper dive into how C++ exception support is implemented. The important thing to remember is that C++ exceptions are faster than checking the return value of a function for an error (as C++ exceptions are implemented using a no overhead algorithm) but are slow when an actual exception is thrown (as the program has to unwind the stack and properly execute each class destructor as needed). For this reason, and others such as maintainability, C++ exceptions should never be used for valid control flow.

Another way that RAII can be used is the finally pattern, which is provided by the C++ Guideline Support Library (GSL). The finally pattern leverages the destructor-only portion of RAII to provide a simple mechanism to perform non-resource-based cleanup when the control flow of a function is complicated or could throw. Consider the following example:

#include <iostream>
#include <stdexcept>

template<typename FUNC>
class finally
{
FUNC m_func;

public:
finally(FUNC func) :
m_func{func}
{ }

~finally()
{
m_func();
}
};

int main(void)
{
auto execute_on_exit = finally{[]{
std::cout << "The answer is: 42\n";
}};
}

In the preceding example, we create a class that is capable of storing a lambda function that is executed when an instance of the finally class loses scope. In this particular case, we output to stdout when the finally class is destroyed. Although this uses a pattern similar to that of RAII, this technically is not RAII as no resource has been acquired.

Also, if a resource does need to be acquired, RAII should be used instead of the finally pattern. The finally pattern, instead, is useful when you are not acquiring a resource but want to execute code when a function returns no matter what control flow path the program takes (a conditional branch or C++ exception).

To demonstrate this, let's look at a more complicated example:

#include <iostream>
#include <stdexcept>

template<typename FUNC>
class finally
{
FUNC m_func;

public:
finally(FUNC func) :
m_func{func}
{ }

~finally()
{
m_func();
}
};

int main(void)
{
try {
auto execute_on_exit = finally{[]{
std::cout << "The answer is: 42\n";
}};

std::cout << "step 1: Collect answers\n";
throw std::runtime_error("???");
std::cout << "step 3: Profit\n";
}
catch (...)
{ }
}

When executed, we get the following:

In the preceding example, we want to ensure that we always output to stdout no matter what the code does. In the middle of execution, we throw an exception, and even though the exception was thrown, our finally code is executed as intended.