Understanding the Meaning of the Term and the Concept - Raii (Resource Acquisition Is Initialization)

Understanding the meaning of the term and the concept - RAII (Resource Acquisition is Initialization)

So why isn't that called "using the stack to trigger cleanup" (UTSTTC:)?

RAII is telling you what to do: Acquire your resource in a constructor! I would add: one resource, one constructor. UTSTTC is just one application of that, RAII is much more.

Resource Management sucks. Here, resource is anything that needs cleanup after use. Studies of projects across many platforms show the majority of bugs are related to resource management - and it's particularly bad on Windows (due to the many types of objects and allocators).

In C++, resource management is particularly complicated due to the combination of exceptions and (C++ style) templates. For a peek under the hood, see GOTW8).


C++ guarantees that the destructor is called if and only if the constructor succeeded. Relying on that, RAII can solve many nasty problems the average programmer might not even be aware of. Here are a few examples beyond the "my local variables will be destroyed whenever I return".

Let us start with an overly simplistic FileHandle class employing RAII:

class FileHandle
{
FILE* file;

public:

explicit FileHandle(const char* name)
{
file = fopen(name);
if (!file)
{
throw "MAYDAY! MAYDAY";
}
}

~FileHandle()
{
// The only reason we are checking the file pointer for validity
// is because it might have been moved (see below).
// It is NOT needed to check against a failed constructor,
// because the destructor is NEVER executed when the constructor fails!
if (file)
{
fclose(file);
}
}

// The following technicalities can be skipped on the first read.
// They are not crucial to understanding the basic idea of RAII.
// However, if you plan to implement your own RAII classes,
// it is absolutely essential that you read on :)



// It does not make sense to copy a file handle,
// hence we disallow the otherwise implicitly generated copy operations.

FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;



// The following operations enable transfer of ownership
// and require compiler support for rvalue references, a C++0x feature.
// Essentially, a resource is "moved" from one object to another.

FileHandle(FileHandle&& that)
{
file = that.file;
that.file = 0;
}

FileHandle& operator=(FileHandle&& that)
{
file = that.file;
that.file = 0;
return *this;
}
}

If construction fails (with an exception), no other member function - not even the destructor - gets called.

RAII avoids using objects in an invalid state. it already makes life easier before we even use the object.

Now, let us have a look at temporary objects:

void CopyFileData(FileHandle source, FileHandle dest);

void Foo()
{
CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

There are three error cases to handled: no file can be opened, only one file can be opened, both files can be opened but copying the files failed. In a non-RAII implementation, Foo would have to handle all three cases explicitly.

RAII releases resources that were acquired, even when multiple resources are acquired within one statement.

Now, let us aggregate some objects:

class Logger
{
FileHandle original, duplex; // this logger can write to two files at once!

public:

Logger(const char* filename1, const char* filename2)
: original(filename1), duplex(filename2)
{
if (!filewrite_duplex(original, duplex, "New Session"))
throw "Ugh damn!";
}
}

The constructor of Logger will fail if original's constructor fails (because filename1 could not be opened), duplex's constructor fails (because filename2 could not be opened), or writing to the files inside Logger's constructor body fails. In any of these cases, Logger's destructor will not be called - so we cannot rely on Logger's destructor to release the files. But if original was constructed, its destructor will be called during cleanup of the Logger constructor.

RAII simplifies cleanup after partial construction.


Negative points:

Negative points? All problems can be solved with RAII and smart pointers ;-)

RAII is sometimes unwieldy when you need delayed acquisition, pushing aggregated objects onto the heap.

Imagine the Logger needs a SetTargetFile(const char* target). In that case, the handle, that still needs to be a member of Logger, needs to reside on the heap (e.g. in a smart pointer, to trigger the handle's destruction appropriately.)

I have never wished for garbage collection really. When I do C# I sometimes feel a moment of bliss that I just do not need to care, but much more I miss all the cool toys that can be created through deterministic destruction. (using IDisposable just does not cut it.)

I have had one particularly complex structure that might have benefited from GC, where "simple" smart pointers would cause circular references over multiple classes. We muddled through by carefully balancing strong and weak pointers, but anytime we want to change something, we have to study a big relationship chart. GC might have been better, but some of the components held resources that should be release ASAP.


A note on the FileHandle sample: It was not intended to be complete, just a sample - but turned out incorrect. Thanks Johannes Schaub for pointing out and FredOverflow for turning it into a correct C++0x solution. Over time, I've settled with the approach documented here.

The meaning of the term - Resource Acquisition Is Initialization

It has been said before (possibly by Scott Meyers, I can't remember), that RAII should be called "Destruction is resource release", or words to that effect.

What "resource acquisition is initialization" literally means is that when an object is constructed (initialized), it acquires some resource (such as a memory allocation or a lock). In other words, it says you should only ever acquire a resource, by initializing some object whose destructor will release it.

This is important to stress because it's a departure from C coding style, where you acquire resources by whatever means a particular API provides (for example malloc(), accept(), or pthread_mutex_lock()), and release them by explicitly calling the corresponding function (for example free(), close(), pthread_mutex_unlock()). The presence of exceptions in C++ makes this approach fairly unworkable. Even in C it results in some tedious code that every use of the API has to write out, and every user has to ensure that control always passes through that code after they're finished using the resource.

But the important part of the pattern is that when the object is destroyed, it releases that resource. It doesn't actually matter whether you acquire the resource by initializing the object, or by doing something else with the object after it has been initialized. And people will still refer to an object as a "RAII object" when there are operations other than initialization that generate the resource(s) managed by the RAII object.

So, don't worry too much about the "acquisition is initialization" in "RAII", because anyway it's slightly misleading.

Why is RAII so named?

First, I should note that it's widely considered a poorly named idiom. Many people prefer SBRM, which stands for Stack Bound Resource Management. Although I (grudgingly) go along with using "RAII" simply because it's widely known and used, I do think SBRM gives a much better description of the real intent.

Second, when RAII was new, it applied as much to the acquisition as releasing of resources. In particular, at the time it was fairly common to see initialization happen in two steps. You'd first define an object, and only afterwards dynamically allocate any resources associated with that object. Many style guides advocated this, largely because at that time (before C++ had exception handling) there was no good way to deal with failure in a constructor. Therefore, the style guides often said, constructors should do only the bare minimum of work, and specifically avoid anything that was open to failure -- especially allocating resources (and a few still say things like that).

Quite a few of those already handled releasing the resources in the destructor though, so that wouldn't have been as clear a distinction from previous practice.

Does RAII support resource ownership transfer?

Does RAII support resource ownership transfer?

It can, yes.

But then it seems that resource ownership transfer also leads to breaking those very 2 properties that seem to define RAII.

Depends a bit on details of how one defines RAII.


The solution is to extend the definition of RAII shown in the question to allow the representation of empty state. If where there is a representation for empty state, then moving ownership of the resource is possible by leaving the source RAII object in such empty state.

The definitions given in the question for construction and destruction are trivial to adjust for this:

  1. Construction either acquires a resource or initialises to empty state. Technically that isn't required, but if empty state is allowed, it's convenient to allow default construction.
  2. Destructor releases resource if and only if it owns any.
  3. Additional definition for moving as described in earlier paragraph.

Most RAII classes in standard library have representation for empty state, and those support transferring their resource. Typical examples of such RAII classes and their empty state:

  • Any dynamic container - a container that contains no elements (and has empty capacity)
  • Any smart pointer - the null value
  • std::fstream - a stream that isn't associated with a file
  • std::thread - a wrapper that isn't associated with a thread

The standard library does also have RAII classes that don't have representation for empty state, and thus cannot support transfer of the resource. An example of such class is std::lock_guard.



I wish someone could also provide a historical perspective

Oldest source for the definition that I have is Stroustrup's book "C++ programming language 3rd ed.". According to wikipedia's estimation RAII was developed around 1984–89, so it would have been a 8-13 year old idea by the time this book was published. Here are most relevant bits that are hopefully not too much to violate copy right:

14.4.1 Using Constructors and Destructors

The technique for managing resources using local objects is usually referred to as "resource acquisition is initialization." This is a general technique that relies on the properties of constructors and destructors and their interaction with exception handling.

...

A constructor tries to ensure that its object is completely and correctly constructed. When that cannot be achieved, a well-written constructor restores - as far as possible - the state of the system to what it was before creation.

14.4.2 Auto_ptr

... auto_ptr, which supports the "resource acquisition is initialization" technique.

Given that std::auto_ptr doesn't necessarily own a resource, and consequently its destructor doesn't in that case release a resource, and it can transfer resource to another instance, and the author who coined RAII considers that std::auto_ptr "supports RAII", I feel confident to say that conflicting the properties described in the question does not disqualify from RAII.

Note that std::auto_ptr was obsoleted by introduction of move semantics in C++11 and has since been removed from the language.

E.3.5.3 Delaying resource acquisition

... resources should be acquired in constructors whenever delayed resource acquisition isn't mandated by the semantics of a class.

I found no explicit description of how RAII relates to ability to transfer ownership of the resource. I suspect that it may be discussed more in later editions written for a language that has move semantics.

How to deal with Resource-waiting in RAII

RAII means that resources are defined by some object. Ideally, the constructor of that object gets the resource, and the destructor releases it. During the time when the object is valid, you can use the object to interact with the resource.

If a resource "needs to be waited on", then by the rules of RAII, that means you don't have an object that represents that resource yet. You instead have an object that represents a resource that will be available in the future.

Which is why C++ calls this type std::future. It's a template, with the argument being the type of the object whose creation you are waiting on.

Conceptually, a future is just a means to forward an object (or exception) from the piece of code that generates it (possibly asynchronously) to the receiver.

Now given your example, we need to remove initialize from MyClass and make that a function which returns a MyClass instance. It could be a static member of MyClass, or it could be just a namespace-scoped function.

So the code would essentially look like this:

auto future = std::async(initialize);

...

if(future.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
{
MyClass resource = future.get(); //The `future` is now empty. Any exceptions will be thrown here.

//use resource
}


Related Topics



Leave a reply



Submit