C++ Succinctly: Resources Acquisition Is Initialization

What Is RAII?

RAII stands for “resource acquisition is initialization.” RAII is a design pattern using C++ code to eliminate resource leaks. Resource leaks happen when a resource that your program acquired is not subsequently released. The most familiar example is a memory leak. Since C++ doesn't have a GC the way C# does, you need to be careful to ensure that dynamically allocated memory is freed. Otherwise, you will leak that memory. Resource leaks can also result in the inability to open a file because the file system thinks it’s already open, the inability to obtain a lock in a multi-threaded program, or the inability to release a COM object.


How Does RAII Work?

RAII works because of three basic facts.

  1. When an automatic storage duration object goes out of scope, its destructor runs.
  2. When an exception occurs, all automatic duration objects that have been fully constructed since the last try-block began are destroyed in the reverse order they were created before any catch handler is invoked.
  3. If you nest try-blocks, and none of the catch handlers of an inner try-block handles that type of exception, then the exception propagates to the outer try-block. All automatic duration objects that have been fully constructed within that outer try-block are then destroyed in reverse creation order before any catch handler is invoked, and so on, until something catches the exception or your program crashes.

RAII helps ensure that you release resources, without exceptions occurring, by simply using automatic storage duration objects that contain the resources. It is similar to the combination of the System.IDisposable interface along with the using statement in C#. Once execution leaves the current block, whether through successful execution or an exception, the resources are freed.

When it comes to exceptions, a key part to remember is that only fully constructed objects are destroyed. If you receive an exception in the midst of a constructor, and the last try block began outside that constructor, since the object isn't fully constructed, its destructor will not run.

This does not mean its member variables, which are objects, will not be destroyed. Any member variable objects that were fully constructed within the constructor before the exception occurred are fully constructed automatic duration objects. Thus, those member objects will be destroyed the same as any other fully constructed objects.

This is why you should always put dynamic allocations inside either std::unique_ptr or std::shared_ptr. Instances of those types become fully constructed objects when the allocation succeeds. Even if the constructor for the object you are creating fails further in, the std::unique_ptr resources will be freed by its destructor and the std::shared_ptr resources will have their reference count decremented and will be freed if the count becomes zero.

RAII isn't about shared_ptr and unique_ptr only, of course. It also applies to other resource types, such as a file object, where the acquisition is the opening of the file and the destructor ensures that the file is properly closed. This is a particularly good example since you only need to create that code right just once—when you write the class—rather than again and again, which is what you need to do if you write the close logic every place you have to open a file.


How Do I Use RAII?

RAII use is described by its name: Acquiring a dynamic resource should complete the initialization of an object. If you follow this one-resource-per-object pattern, then it is impossible to wind up with a resource leak. Either you will successfully acquire the resource, in which case the object that encapsulates it will finish construction and be subject to destruction, or the acquisition attempt will fail, in which case you did not acquire the resource; thus, there is no resource to release.

The destructor of an object that encapsulates a resource must release that resource. This, among other things, is one of the important reasons why destructors should never throw exceptions, except those they catch and handle within themselves.

If the destructor threw an uncaught exception, then, to quote Bjarne Stroustrup, “All kinds of bad things are likely to happen because the basic rules of the standard library and the language itself will be violated. Don't do it.”

As he said, don’t do it. Make sure you know what exceptions, if any, everything you call in your destructors could throw so you can ensure that you handle them properly.

Now you might be thinking that if you follow this pattern, you will end up writing a ton of classes. You will occasionally write an extra class here and there, but you aren’t likely to write too many because of smart pointers. Smart pointers are objects too. Most types of dynamic resources can be put into at least one of the existing smart pointer classes. When you put a resource acquisition inside a suitable smart pointer, if the acquisition succeeds, then that smart pointer object will be fully constructed. If an exception occurs, then the smart pointer object’s destructor will be called, and the resource will be freed.

There are several important smart pointer types. Let’s have a look at them.

The std::unique_ptr Function

The unique pointer, std::unique_ptr, is designed to hold a pointer to a dynamically allocated object. You should use this type only when you want one pointer to the object to exist. It is a template class that takes a mandatory and an optional template argument. The mandatory argument is the type of the pointer it will hold. For instance auto result = std::unique_ptr<int>(new int()); will create a unique pointer that contains an int*. The optional argument is the type of deleter. We see how to write a deleter in a coming sample. Typically, you can avoid specifying a deleter since the default_deleter, which is provided for you if no deleter is specified, covers almost every case you can imagine.

A class that has std::unique_ptr as a member variable cannot have a default copy constructor. Copy semantics are disabled for std::unique_ptr. If you want a copy constructor in a class that has a unique pointer, you must write it. You should also write an overload for the copy operator. Normally, you want std::shared_ptr in that case.

However, you might have something like an array of data. You may also want any copy of the class to create a copy of the data as it exists at that time. In that case, a unique pointer with a custom copy constructor could be the right choice.

std::unique_ptr is defined in the <memory> header file.

std::unique_ptr has four member functions of interest.

The get member function returns the stored pointer. If you need to call a function that you need to pass the contained pointer to, use get to retrieve a copy of the pointer.

The release member function also returns the stored pointer, but release invalidates the unique_ptr in the process by replacing the stored pointer with a null pointer. If you have a function where you want to create a dynamic object and then return it, while still maintaining exception safety, use std:unique_ptr to store the dynamically created object, and then return the result of calling release. This gives you exception safety while allowing you to return the dynamic object without destroying it with the std::unique_ptr’s destructor when the control exits from the function upon returning the released pointer value at the end.

The swap member function allows two unique pointers to exchange their stored pointers, so if A is holding a pointer to X, and B is holding a pointer to Y, the result of calling A::swap(B); is that A will now hold a pointer to Y, and B will hold a pointer to X. The deleters for each will also be swapped, so if you have a custom deleter for either or both of the unique pointers, be assured that each will retain its associated deleter.

The reset member function causes the object pointed to by the stored pointer, if any, to be destroyed in most cases. If the current stored pointer is null, then nothing is destroyed. If you pass in a pointer to the object that the current stored pointer points to, then nothing is destroyed. You can choose to pass in a new pointer, nullptr, or to call the function with no parameters. If you pass in a new pointer, then that new object is stored. If you pass in nullptr, then the unique pointer will store null. Calling the function with no parameters is the same as calling it with nullptr.

The std::shared_ptr Function

The shared pointer, std::shared_ptr, is designed to hold a pointer to a dynamically allocated object and to keep a reference count for it. It is not magic; if you create two shared pointers and pass them each a pointer to the same object, you will end up with two shared pointers—each with a reference count of 1, not 2. The first one that is destroyed will release the underlying resource, giving catastrophic results when you try to use the other one or when the other one is destroyed and tries to release the already released underlying resource.

To use the shared pointer properly, create one instance with an object pointer and then create all other shared pointers for that object from an existing, valid shared pointer for that object. This ensures a common reference count, so the resource will have a proper lifetime. Let’s look at a quick sample to see the right and wrong ways to create shared_ptr objects.

Sample: SharedPtrSample\SharedPtrSample.cpp

std::shared_ptr is defined in the <memory> header file.

std::shared_ptr has five member functions of interest.

The get member function works the same as the std::unique_ptr::get member function.

The use_count member function returns a long, which tells you what the current reference count for the target object is. This does not include weak references.

The unique member function returns a bool, informing you whether this particular shared pointer is the sole owner of the target object.

The swap member function works the same as the std::unique_ptr::swap member function, with the addition that the reference counts for the resources stay the same.

The reset member function decrements the reference count for the underlying resource and destroys it if the resource count becomes zero. If a pointer to an object is passed in, the shared pointer will store it and begin a new reference count for that pointer. If nullptr is passed in, or if no parameter is passed, then the shared pointer will store null.

The std::make_shared Function

The std::make_shared template function is a convenient way to construct an initial std::shared_ptr. As we saw previously in SharedPtrSample, you pass the type as the template argument and then simply pass in the arguments, if any, for the desired constructor. std::make_shared will construct a heap instance of the template argument object type and make it into a std::shared_ptr. You can then pass that std::shared_ptr as an argument to the std::shared_ptr constructor to create more references to that shared object.


ComPtr in WRL for Metro-Style Apps

The Windows Runtime Template Library (WRL) provides a smart pointer named ComPtr within the Microsoft::WRL namespace for use with COM objects in Windows 8 Metro-style applications. The pointer is found in the <wrl/client.h> header, as part of the Windows SDK (minimum version 8.0).

Most of the operating system functionality that you can use in Metro-style applications is exposed by the Windows Runtime (“WinRT”). WinRT objects provide their own automatic reference counting functionality for object creation and destruction. Some system functionality, such as Direct3D, requires you to directly use and manipulate it through classic COM. ComPtr handles COM’s IUnknown-based reference counting for you. It also provides convenient wrappers for QueryInterface and includes other functionality that is useful for smart pointers.

The two member functions you typically use are As to get a different interface for the underlying COM object and Get to take an interface pointer to the underlying COM object that the ComPtr holds (this is the equivalent of std::unique_ptr::get).

Sometimes you will use Detach, which works the same way as std::unique_ptr::release but has a different name because release in COM implies decrementing the reference count and Detach does not do that.

You might use ReleaseAndGetAddressOf for situations where you have an existing ComPtr that could already hold a COM object and you want to replace it with a new COM object of the same type. ReleaseAndGetAddressOf does the same thing as the GetAddressOf member function, but it first releases its underlying interface, if any.


Exceptions in C++

Unlike .NET, where all exceptions derive from System.Exception and have guaranteed methods and properties, C++ exceptions are not required to derive from anything; nor are they even required to be class types. In C++, throw L"Hello World!"; is perfectly acceptable to the compiler as is throw 5;. Basically, exceptions can be anything.

That said, many C++ programmers will be unhappy to see an exception that does not derive from std::exception (found in the <exception> header). Deriving all exceptions from std::exception provides a way to catch exceptions of unknown type and retrieve information from them via the what member function before re-throwing them. std::exception::what takes no parameters and returns a const char* string, which you can view or log so you know what caused the exception.

There is no stack trace—not counting the stack-trace capabilities your debugger provides—with C++ exceptions. Because automatic duration objects within the scope of the try-block that catches the exception are automatically destroyed before the appropriate catch handler, if any, is activated, you do not have the luxury of examining the data that may have caused the exception. All you have to work with initially is the message from the what member function.

If it is easy to recreate the conditions that led to the exception, you can set a breakpoint and rerun the program, allowing you to step through execution of the trouble area and possibly spot the issue. Because that is not always possible, it is important to be as precise as you can with the error message.

When deriving from std::exception, you should make sure to override the what member function to provide a useful error message that will help you and other developers diagnose what went wrong.

Some programmers use a variant of a rule stating that you should always throw std::exception-derived exceptions. Remembering that the entry point (main or wmain) returns an integer, these programmers will throw std::exception-derived exceptions when their code can recover, but will simply throw a well-defined integer value if the failure is unrecoverable. The entry-point code will be wrapped in a try-block that has a catch for an int. The catch handler will return the caught int value. On most systems, a return value of 0 from a program means success. Any other value means failure.

If there is a catastrophic failure, then throwing a well-defined integer value other than 0 can help provide some meaning. Unless you are working on a project where this is the preferred style, you should stick to std::exception-derived exceptions, since they let programs handle exceptions using a simple logging system to record messages from exceptions not handled, and they perform any cleanup that is safe. Throwing something that doesn’t derive from std::exception would interfere with these error-logging mechanisms.

One last thing to note is that C#’s finally construct has no equivalent in C++. The RAII idiom, when properly implemented, makes it unnecessary since everything will have been cleaned up.


C++ Standard Library Exceptions

We’ve already discussed std::exception, but there are more types than that available in the standard library, and there is additional functionality to explore. Let’s look at the functionality from the <exception> header file first.

The std::terminate function, by default, lets you crash out of any application. It should be used sparingly, since calling it rather than throwing an exception will bypass all normal exception handling mechanisms. If you wish, you can write a custom terminate function without parameters and return values. An example of this will be seen in ExceptionsSample, which is coming.

To set the custom terminate, you call std::set_terminate and pass it the address of the function. You can change the custom terminate handler at any time; the last function set is what will be called in the event of either a call to std::terminate or an unhandled exception. The default handler calls the abort function from the <cstdlib> header file.

The <stdexcept> header provides a rudimentary framework for exceptions. It defines two classes that inherit from std::exception. Those two classes serve as the parent class for several other classes.

The std::runtime_error class is the parent class for exceptions thrown by the runtime or due to a mistake in a C++ Standard Library function. Its children are the std::overflow_error class, the std::range_error class, and the std::underflow_error class.

The std::logic_error class is the parent class for exceptions thrown due to programmer error. Its children are the std::domain_error class, the std::invalid_argument class, the std::length_error class, and the std::out_of_range class.

You can derive from these classes or create your own exception classes. Coming up with a good exception hierarchy is a difficult task. On one hand, you want exceptions that will be specific enough that you can handle all exceptions based on your knowledge at build-time. On the other hand, you do not want an exception class for each error that could occur. Your code would end up bloated and unwieldy, not to mention the waste of time writing catch handlers for every exception class.

Spend time at a whiteboard, or with a pen and paper, or however you want thinking about the exception tree your application should have.

The following sample contains a class called InvalidArgumentExceptionBase, which is used as the parent of a template class called InvalidArgumentException. The combination of a base class, which can be caught with one exception handler, and a template class, which allows us to customize the output diagnostics based on the type of the parameter, is one option for balancing between specialization and code bloat.

The template class might seem confusing right now; we will be discussing templates in an upcoming chapter, at which point anything currently unclear should clear up.

Sample: ExceptionsSample\InvalidArgumentException.h

Sample: ExceptionsSample\ExceptionsSample.cpp

Though I mention it in the comments, I just wanted to point out again that you will not see the custom terminate function run unless you run this sample from a command prompt. If you run it in Visual Studio, the debugger will intercept the program and orchestrate its own termination after giving you a chance to examine the state to see if you can determine what went wrong. Also, note that this program will always crash. This is by design since it allows you to see the terminate handler in action.

Conclusion

As we saw in this article, RAII helps ensure that you release resources, without exceptions occurring, by simply using automatic storage duration objects that contain the resources. In the next installment of this series, we zoom in on pointers and references.

This lesson represents a chapter from C++ Succinctly, a free eBook from the team at Syncfusion.
Tags:

Comments

Related Articles