Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Advanced C++ Programming Cookbook

You're reading from   Advanced C++ Programming Cookbook Become an expert C++ programmer by mastering concepts like templates, concurrency, and type deduction

Arrow left icon
Product type Paperback
Published in Jan 2020
Publisher Packt
ISBN-13 9781838559915
Length 454 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Dr. Rian Quinn Dr. Rian Quinn
Author Profile Icon Dr. Rian Quinn
Dr. Rian Quinn
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Getting Started with Library Development 2. Using Exceptions for Error Handling FREE CHAPTER 3. Implementing Move Semantics 4. Using Templates for Generic Programming 5. Concurrency and Synchronization 6. Optimizing Your Code for Performance 7. Debugging and Testing 8. Creating and Implementing Your Own Container 9. Exploring Type Erasure 10. An In-Depth Look at Dynamic Allocation 11. Common Patterns in C++ 12. A Closer Look at Type Deduction 13. Bonus - Using C++20 Features 14. Other Books You May Enjoy

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.

You have been reading a chapter from
Advanced C++ Programming Cookbook
Published in: Jan 2020
Publisher: Packt
ISBN-13: 9781838559915
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.
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 $19.99/month. Cancel anytime
Banner background image