RAII and exceptions

The Partridge Family were neither partridges nor a family. Discuss.
Post Reply
albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

RAII and exceptions

Post by albinopapa » April 14th, 2020, 6:13 pm

If anyone is wondering what RAII is, it stands for Resource Acquisition Is Initialization, but if you're like me have no idea what that really means. So let's go over a few things. RAII is the idea of using constructors and destructors to manage the allocation and deallocation of a resource. A resource can be anything that is allocated on the heap using C's malloc or C++ operator new or even std::allocator. A resource could also be something like a network socket or C's FILE pointer. Anything that needs to be created and destroyed separately should be created in the constructor and destroyed in the destructor for automatic cleanup.

There are a few reasons to stick with this idiom. The first is to simplify the code by removing the management of a resource from the main logic of the program. The second is for safety. The idiom prevents memory or other resource leaks from happening. If you are managing resources manually, there is a chance no matter how much care is taken that you forget to release or delete the resource somewhere or allocate to the same pointer without first deleting that memory first for example. Also, there are those exceptions. Exceptions unwind the call stack and calls destructors for any object that has been allocated on the stack up to that point. If you are managing resources manually, you either need to be aware of cases where an exception may occur so that you can clean up any allocated resources or risk having those leaks. Some things might be fine like allocated memory since all program memory as far as I'm aware of is release back to the OS when the program exits.

There are however a few cases where having an exception thrown can cause unexpected behavior. Let's say you have a class A and you are allocating memory in the constructor and deallocating in the destructor. Now let's say that you also have a socket in class A that will write it's incoming and outgoing data to the heap allocated buffer in A. Now you open your socket and then try allocating memory and for some reason or another the allocation throws an exception.

Will the destructor of A be called to close the socket? NOPE

The logic behind this is A was never fully constructed so no need to call the destructor, but it does call destructors for any class members in class A. So what do you do so that you can close the socket or free the memory if an exception is thrown before the object is fully instantiated? Well, it goes back to RAII.

For each resource, you should have it wrapped in a class. For example, a Socket class for sockets where upon destruction the socket is closed. Using the standard library std::shared_ptr, std::unique_ptr, std::string or even std::vector are all examples of RAII for memory related resources. Std::ifstream and std::ofstream are basically RAII wrappers for FILE*.

I bring t his up because of an issue that has happened at least twice now and brought up on the forums. The management of the Windows window creation. Here's the scenario as I can tell. I'll use the Chili framework 2016 code as I'm more familiar with it than the Hardware 3D framework right now.

In this framework, chili uses MainWindow as a way of managing the creation and destruction of a window along with some other tasks. In the constructor of MainWindow an exception could be thrown if the creation of the hWnd handle is not valid after calling CreateWindowEx. There is a problem though. Before calling CreateWindowEx, a WNDCLASSEX object is registered with the OS using RegisterClassEx. The reason I say this is a problem is this registers message handling routines with the operating system and must be unregistered when the window is closed to remove the message handling routine. This has been an issue because if the MainWindow constructor throws it's destructor where the call to UnregisterClass function is never called. In the two cases I mentioned messages where still being forwarded to the message handlers, but there was no object to handle incoming messages.

This makes the WNDCLASSEX object or at the very least the registering and unregistering of the object should be it's own class that is stored in MainWindow. That way if MainWindow::MainWindow throws, the message handling routine is unregistered and the program can close properly.

So what am I trying to say?

For any resource they should be a part of it's own class separate from the class it's used in.

For every Open/Close pair, create a resource wrapper.
For every New/Delete pair, create a resource wrapper or use one of the STL implementations.
For every resource that has separate create/destroy methods there needs to be a wrapper for it that will manage it's lifetime from creation to deletion.

Even if you are the most careful of programmers, there's still the "exception".
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

User avatar
chili
Site Admin
Posts: 3948
Joined: December 31st, 2011, 4:53 pm
Location: Japan
Contact:

Re: RAII and exceptions

Post by chili » April 15th, 2020, 10:04 am

Yeah, I like to use RAII to manage setup/cleanup pairs. You could just put a bunch of calls in the ctor(s) / dtor of your main class (app or whatever), but it's just cleaner and more organized to put that in a separate class and then embed in app. Exceptions go hand in hand with this whole setup.

One other very interesting implication of exceptions (not so much RAII) is the idea of exception guarantees / strong exception guarantee. If you understand that, it can help you understand many of the design decisions in the standard library.
Chili

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: RAII and exceptions

Post by albinopapa » April 15th, 2020, 5:21 pm

Exception safety
After the error condition is reported by a function, additional guarantees may be provided with regards to the state of the program. The following four levels of exception guarantee are generally recognized, which are strict supersets of each other:
  1. Nothrow (or nofail) exception guarantee -- the function never throws exceptions. Nothrow (errors are reported by other means or concealed) is expected of destructors and other functions that may be called during stack unwinding. The destructors are noexcept by default. (since C++11) Nofail (the function always succeeds) is expected of swaps, move constructors, and other functions used by those that provide strong exception guarantee.
  2. Strong exception guarantee -- If the function throws an exception, the state of the program is rolled back to the state just before the function call. (for example, std::vector::push_back)
  3. Basic exception guarantee -- If the function throws an exception, the program is in a valid state. No resources are leaked, and all objects' invariants are intact.
  4. No exception guarantee -- If the function throws an exception, the program may not be in a valid state: resource leaks, memory corruption, or other invariant-destroying errors may have occurred.
    Generic components may, in addition, offer exception-neutral guarantee: if an exception is thrown from a template parameter (e.g. from the Compare function object of std::sort or from the constructor of T in std::make_shared), it is propagated, unchanged, to the caller.
Had to look up what you meant by strong exception guarantee.

This is something that I've always expected to be followed by library writers as they have no idea what the end user is going to do with their code. App developers on the other hand probably don't even think about the state of their program after an exception ( some probably do though ) aside from logging information that might help them write a patch later on.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

User avatar
chili
Site Admin
Posts: 3948
Joined: December 31st, 2011, 4:53 pm
Location: Japan
Contact:

Re: RAII and exceptions

Post by chili » April 16th, 2020, 8:25 am

It's more common in general utilities for sure. A good example of this stuff can be found in the C++ Concurrency book.
Chili

Post Reply