Does RAII support resource ownership transfer?

Viewed 344

I mainly used to think about RAII as being about using object lifetime to avoid leaking resources and that served me well enough in practice. But I recently had some discussions about what exactly constitutes an RAII pattern and what does not, which made me search for more definitions and commentaries online, which ended up adding more confusion than clarity.

The standard definition of an RAII class seems to require two properties:

  1. The constructor of an RAII class should acquire the resources or throw an exception if it fails in that process.
  2. The destructor of an RAII class should release the resources.

But then I've also seen mentioned in some RAII definitions that resource ownership can be "safely transferred" between instances of such RAII classes. So resource ownership transfer seems to be accepted as part of the RAII pattern.

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

Let's say that I have two instances of an RAII class - Instance_Source and Instance_Destination - and that I transfer ownership of the underlying resource(s) from Instance_Source to Instance_Destination. Then we have:

  • Conflict with Property 2:
    • The destructor of Instance_Source will not release any resource, so we now broke the requirement that destructors should release resources.
  • Conflicts with Property 1:
    • One advantage of doing the resource acquiring in the initialization is that I can use instances knowing that they contain valid resources (otherwise they would not have been constructed). But with the ownership transfer, I am now left with instances that no longer contain valid resources, which eliminates the principal advantage that acquiring resources in the constructor was supposed to bring.
    • There are also scenarios in which I will not want Instance_Destination to acquire any resource during its construction because I want it to only get ownership of what Instance_Source acquired, under special conditions. To support such scenarios, I would have to break the requirement that constructors acquire resources, to allow initializing Instance_Destination without acquiring any resource.

So in scenarios in which I need to allow resource ownership transfer, I find that I have to "play loose" with the 2 RAII requirements about acquiring resources in the constructors and releasing them in destructors. And this works well enough in practice, but does it still constitute an RAII pattern in theory?

This is what led me to my question: Does RAII support resource ownership transfer?

If the answer is yes, then it looks like most RAII definitions should be reworked to not rely on what the constructors and the destructors are supposed to do with resources.

If the answer is no, then this should be highlighted as an important limitation of RAII.

3 Answers

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.

I feel that your analysis is based on an attempt to think of the "rules" of RAII as the definition of RAII. You seem to be trying to treat RAII like it is a program that either is executed correctly or is not.

RAII, like any programming idiom, is a principle that exists for a purpose. The purpose of RAII is to more accurately ensure the cleanup of resources that need cleaning up. The principle of RAII is to bind such resource cleanup to something which is bounded to a particular program scope. The way you do this in C++ is with constructors and destructors of stack objects (or objects otherwise owned/managed by stack objects indirectly, since RAII can be nested) which represent the scope to which the resources are bound.

But that's just how C++ gets it done. Or rather, one version of C++.

Does transferring ownership of resources violate the principle of RAII? No; the cleanup of those resources will still happen by mechanisms that are governed by programmatic scoping. The scope may be bigger overall thanks to the transfer, but it is still bounded.

Does transferring ownership of resources violate the purpose of RAII? No; any cleanup of resources will still happen.

Does this violate some reading of the rules of RAII? Maybe, but we didn't invent RAII to lock ourselves into the low-level details of how RAII gets used in one version of one language. We invented RAII to solve a problem. And transferring ownership does not stop RAII from solving that problem.

Constructors and destructors are mechanisms of the RAII technique, they are not the purpose of this idiom.

To better understand RAII you need to understand where the need for RAII came from and what problem does RAII actually solve.

The original problem

For this consider C. You have a library that thought it's API it gives you a resource:

const char * some_lib_get_last_error_message();

The resource you are given is the string denoted by the pointer returned by the function. Now answer this question: is the object a resource needing manual lifetime management? If so who is responsible for its creation/destruction? A raw pointer is not powerful enough to express this properties so the question is impossible to answer (without looking at the implementation or documentation).

The string could be an object with static storage duration. In this case the user of the library must not "clean" this resource in any way. Doing so (e.g. free) would cause serious problems. Or the object could be an object with dynamic storage duration. If that is the case then we have another dilemma: who is responsible to clean it and how is the cleaning supposed to be done. It could be that the library cleans it somehow. If that is the case the user cleaning it would result in serious problems. It could be that the user must clean it. In this case if the user forgets to clean it then that would result in serious problems. Then there is the problem of how the user is supposed to clean it. It could require a free or it could require a call to some other library API. Moreover there could be other conditions that govern when the cleaning of this resource can be done. The user could be required to do some other actions before cleaning it or it could be required to not clean it before some other events.

This is a problem in C and the way it's handled is through documentation. The library must document every resource the user must release, how to do so and when it is allowed/required to do so.

The problem is aggravated in C++

With C++ the problem becomes even more serious: because of exceptions C++ has many many hidden exit points from a function. So ensuring a resource is released as needed becomes near impossible with the C idiom. Consider:

auto user_function()
{
    auto resource_r1 = acquire_resource_r1();

    A a = foo(X{}, Y{}, resource_r1);

    auto resource_r2 = acquire_resource_r2();
    B b = bar("text", resource_r2);

    C c = a + b;
    c.use(resource_r1, resournce_r2);

    release_r1(resource_r1);
    release_r2(resource_r2)
}

While this would have been fine in C, in C++ this is incorrect code. Depending on the types involved: acquire_resournce_r1 could throw, X constructor could throw, Y constructor could throw, the conversion from X to the 1st param type of foo could throw, the conversion from Y to the 2nd param type of foo could throw, the conversion from resource_r1 to the 3rd param type of A could throw, foo could throw, acquire_resource_2 could throw, the conversion from the type returned from foo to A could throw, the constructor of the 1st param type of bar could throw, the conversion from resource_r2 to the 2nd param type of bar could throw, bar could throw, the conversion from the type returned by bar to b could throw, the operator + could throw, the conversion from the type returned by + to C could throw, the conversion from resource_r1 to the 1st param type of use could throw, the conversion from resource_r2 to the 1st param type of use could throw, use could throw, release_r1 could throw, release_r2 could throw. Yes, there can be about 20 hidden exit points in the above function.

So how do you ensure that resource_r1 and resource_r2 are always correctly cleaned? Using the C idiom to do so would be nothing short of a nightmare. Think what kind of try/catch monstrosity would be required here to correctly clean resource_r1 an resource_r2. It would also completely defeat the purpose of exceptions as you would need to catch exceptions just to correctly do the cleanup, even if you can't nor want to treat the errors in any way.

Not to mention that it would still have the same problem as in C: you don't know who's responsible for cleaning the resources.

The C++ solution

Bjarne Stroustrup and Andrew Koenig came with an ingenious and elegant solution: bind the lifetime of the resources to the lifetime of objects. And use objects with automatic, thread or static storage duration. An object has two lifetime events: construction and destruction; this are done automagically by the compiler in the case of objects with automatic, thread and static storage duration. A resource has two lifetime events: acquisition and release; these need to be done manually. So RAII binds the acquisition of a resource to the construction of an object and the release of a resource to the destruction of the resource. Now the compiler will do everything for you: it will properly clean the resource.. correctly.. regardless of exceptions thrown.. or order of acquisition. Not only this is done correctly, but the user of the resource must not bother at all with the manual management of the resource lifetime.

This brings us to the concept of ownership. Remember how in C there was no clear entity responsible with the destruction of the resource. With RAII there is always (at least) an owner of every resource: the object to which the lifetime of the resource is bound to. This not only solves the problem of "who must clean" but also "how to clean". The owner of the resource is responsible with the cleaning and knows how to clean the resource.

The most important concept in RAII is ownership. As long as a resource has an owner the resource acquisition/release is always done correctly.

Conclusion

So, so sum it up:

  • A resource must always be owned by an object (or by multiple objects in a shared ownership).
  • The most common point to acquire the resource is in the object construction. However that is not always the case. An object can be created without a resource and the resource can be acquired during the lifetime of the object.
  • The most common point to release the resource is in the destructor of the object that owns it. However that is not always the case. The resource can be manually released by the user before the destruction of the object. Regardless, the object type that is a resource owner must always check in the destructor if it still owns a resource and clean it if it does.
  • the owner of a resource can change during the lifetime of a resource. Objects can swap or steal resources from one another. As long as no resource ends up orphan (without an owner) everting is ok.
  • the C++ mechanisms of destructors, object lifetime, scope exit, order of initialization and stack unwinding are leveraged by RAII to ensure always proper and correct resource cleanup, without any action from the user.
Related