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.
- When an automatic storage duration object goes out of scope, its destructor runs.
- 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.
- 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
#include <memory> #include <iostream> #include <ostream> #include "../pchar.h" using namespace std; struct TwoInts { TwoInts(void) : A(), B() { } TwoInts(int a, int b) : A(a), B(b) { } int A; int B; }; wostream& operator<<(wostream& stream, TwoInts* v) { stream << v->A << L" " << v->B; return stream; } int _pmain(int /*argc*/, _pchar* /*argv*/[]) { //// Bad: results in double free. //try //{ // TwoInts* p_i = new TwoInts(10, 20); // auto sp1 = shared_ptr<TwoInts>(p_i); // auto sp2 = shared_ptr<TwoInts>(p_i); // p_i = nullptr; // wcout << L"sp1 count is " << sp1.use_count() << L"." << endl << // L"sp2 count is " << sp2.use_count() << L"." << endl; //} //catch(exception& e) //{ // wcout << L"There was an exception." << endl; // wcout << e.what() << endl << endl; //} //catch(...) //{ // wcout << L"There was an exception due to a double free " << // L"because we tried freeing p_i twice!" << endl; //} // This is one right way to create shared_ptrs. { auto sp1 = shared_ptr<TwoInts>(new TwoInts(10, 20)); auto sp2 = shared_ptr<TwoInts>(sp1); wcout << L"sp1 count is " << sp1.use_count() << L"." << endl << L"sp2 count is " << sp2.use_count() << L"." << endl; wcout << L"sp1 value is " << sp1 << L"." << endl << L"sp2 value is " << sp2 << L"." << endl; } // This is another right way. The std::make_shared function takes the // type as its template argument, and then the argument value(s) to the // constructor you want as its parameters, and it automatically // constructs the object for you. This is usually more memory- // efficient, as the reference count can be stored with the // shared_ptr's pointed-to object at the time of the object's creation. { auto sp1 = make_shared<TwoInts>(10, 20); auto sp2 = shared_ptr<TwoInts>(sp1); wcout << L"sp1 count is " << sp1.use_count() << L"." << endl << L"sp2 count is " << sp2.use_count() << L"." << endl; wcout << L"sp1 value is " << sp1 << L"." << endl << L"sp2 value is " << sp2 << L"." << endl; } return 0; }
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
#pragma once #include <exception> #include <stdexcept> #include <string> #include <sstream> namespace CppForCsExceptions { class InvalidArgumentExceptionBase : public std::invalid_argument { public: InvalidArgumentExceptionBase(void) : std::invalid_argument("") { } virtual ~InvalidArgumentExceptionBase(void) throw() { } virtual const char* what(void) const throw() override = 0; }; template <class T> class InvalidArgumentException : public InvalidArgumentExceptionBase { public: inline InvalidArgumentException( const char* className, const char* functionSignature, const char* parameterName, T parameterValue ); inline virtual ~InvalidArgumentException(void) throw(); inline virtual const char* what(void) const throw() override; private: std::string m_whatMessage; }; template<class T> InvalidArgumentException<T>::InvalidArgumentException( const char* className, const char* functionSignature, const char* parameterName, T parameterValue) : InvalidArgumentExceptionBase(), m_whatMessage() { std::stringstream msg; msg << className << "::" << functionSignature << " - parameter '" << parameterName << "' had invalid value '" << parameterValue << "'."; m_whatMessage = std::string(msg.str()); } template<class T> InvalidArgumentException<T>::~InvalidArgumentException(void) throw() { } template<class T> const char* InvalidArgumentException<T>::what(void) const throw() { return m_whatMessage.c_str(); } }
Sample: ExceptionsSample\ExceptionsSample.cpp
#include <iostream> #include <ostream> #include <memory> #include <exception> #include <stdexcept> #include <typeinfo> #include <algorithm> #include <cstdlib> #include "InvalidArgumentException.h" #include "../pchar.h" using namespace CppForCsExceptions; using namespace std; class ThrowClass { public: ThrowClass(void) : m_shouldThrow(false) { wcout << L"Constructing ThrowClass." << endl; } explicit ThrowClass(bool shouldThrow) : m_shouldThrow(shouldThrow) { wcout << L"Constructing ThrowClass. shouldThrow = " << (shouldThrow ? L"true." : L"false.") << endl; if (shouldThrow) { throw InvalidArgumentException<const char*>( "ThrowClass", "ThrowClass(bool shouldThrow)", "shouldThrow", "true" ); } } ~ThrowClass(void) { wcout << L"Destroying ThrowClass." << endl; } const wchar_t* GetShouldThrow(void) const { return (m_shouldThrow ? L"True" : L"False"); } private: bool m_shouldThrow; }; class RegularClass { public: RegularClass(void) { wcout << L"Constructing RegularClass." << endl; } ~RegularClass(void) { wcout << L"Destroying RegularClass." << endl; } }; class ContainStuffClass { public: ContainStuffClass(void) : m_regularClass(new RegularClass()), m_throwClass(new ThrowClass()) { wcout << L"Constructing ContainStuffClass." << endl; } ContainStuffClass(const ContainStuffClass& other) : m_regularClass(new RegularClass(*other.m_regularClass)), m_throwClass(other.m_throwClass) { wcout << L"Copy constructing ContainStuffClass." << endl; } ~ContainStuffClass(void) { wcout << L"Destroying ContainStuffClass." << endl; } const wchar_t* GetString(void) const { return L"I'm a ContainStuffClass."; } private: unique_ptr<RegularClass> m_regularClass; shared_ptr<ThrowClass> m_throwClass; }; void TerminateHandler(void) { wcout << L"Terminating due to unhandled exception." << endl; // If you call abort (from <cstdlib>), the program will exit // abnormally. It will also exit abnormally if you do not call // anything to cause it to exit from this method. abort(); //// If you were instead to call exit(0) (also from <cstdlib>), //// then your program would exit as though nothing had //// gone wrong. This is bad because something did go wrong. //// I present this so you know that it is possible for //// a program to throw an uncaught exception and still //// exit in a way that isn't interpreted as a crash, since //// you may need to find out why a program keeps abruptly //// exiting yet isn't crashing. This would be one such cause //// for that. //exit(0); } int _pmain(int /*argc*/, _pchar* /*argv*/[]) { // Set a custom handler for std::terminate. Note that this handler // won't run unless you run it from a command prompt. The debugger // will intercept the unhandled exception and will present you with // debugging options when you run it from Visual Studio. set_terminate(&TerminateHandler); try { ContainStuffClass cSC; wcout << cSC.GetString() << endl; ThrowClass tC(false); wcout << L"tC should throw? " << tC.GetShouldThrow() << endl; tC = ThrowClass(true); wcout << L"tC should throw? " << tC.GetShouldThrow() << endl; } // One downside to using templates for exceptions is that you need a // catch handler for each specialization, unless you have a base // class they all inherit from, that is. To avoid catching // other std::invalid_argument exceptions, we created an abstract // class called InvalidArgumentExceptionBase, which serves solely to // act as the base class for all specializations of // InvalidArgumentException<T>. Now we can catch them all, if desired, // without needing a catch handler for each. If you wanted to, however, // you could still have a handler for a particular specialization. catch (InvalidArgumentExceptionBase& e) { wcout << L"Caught '" << typeid(e).name() << L"'." << endl << L"Message: " << e.what() << endl; } // Catch anything derived from std::exception that doesn’t already // have a specialized handler. Since you don't know what this is, you // should catch it, log it, and re-throw it. catch (std::exception& e) { wcout << L"Caught '" << typeid(e).name() << L"'." << endl << L"Message: " << e.what() << endl; // Just a plain throw statement like this is a re-throw. throw; } // This next catch catches everything, regardless of type. Like // catching System.Exception, you should only catch this to // re-throw it. catch (...) { wcout << L"Caught unknown exception type." << endl; throw; } // This will cause our custom terminate handler to run. wcout << L"tC should throw? " << ThrowClass(true).GetShouldThrow() << endl; return 0; }
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.
Comments