Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 3.  Resource Encapsulation


3.5. RAII Types

The term Resource Acquisition Is Initialization (RAII[5]) is about the best tongue twister in the business, but it is a remarkably simple concept. All it means is that the initialization (via a call to one of the constructors) of an object involves the acquisition of the resource that the object will manage. The implicit complement is that the uninitialization (via a call to the destructor) of the object will (automatically) result in the resource being released (see Resource Release Is Destruction).

[5] I think the pronunciation of this one is open for debate: "rah-ee," "rye," and "ray" are all recognized, but I go for the full glottis-confronting "R-A-I-I."

Definition: Resource Acquisition Is Initialization is a mechanism that takes advantage of C++'s support for construction and automatic destruction to ensure the deterministic release of resources associated with an instance of an encapsulating type. It can be thought of as a superset of the RRID mechanism.


It is this mechanism that allows objects to clear up after themselves—actually it's the compiler, the author of the class and, sometimes, the machine itself who do it—rather than you having to do it. This is the essence of the power of C++'s RAII support. However the object comes to be destroyed, its destructor will be called.

Objects allocated on the stack, as automatic variables, are destroyed automatically when they go out of scope. Furthermore, the order of destruction is the reverse of the order of construction. Objects allocated on the heap are destroyed when they are explicitly deleted (via invocation of delete or by explicit destruction).

The destruction of automatic variables happens irrespective of the manner in which the scope is exited. This may be because execution has reached the end of the scope, or because a return statement was encountered, or because an exception was thrown, or as a result of a goto statement; in all cases the destructors are executed. Clearly this requires a little housekeeping work from the compiler, but this is highly optimized and very efficient. The point is that this guaranteed and deterministic destruction of objects presents an extremely powerful tool, which will feature strongly throughout the book. We'll now look at different flavors of RAII.

3.5.1 Immutable / Mutable

For some RAII types, the resource is acquired in the constructor and released in the destructor, and no manipulations of the encapsulating instance between these two times cause the encapsulation relationship to be altered. This is immutable RAII and is, in my opinion, the best form of resource encapsulation, as it provides the simplest semantics to the writing of such types and to their use, as shown in Listing 3.8.

Listing 3.8.


template <typename T>


class scoping_ptr


{


public:


  scoping_ptr(T *p)


    : m_ptr(p)


  {}


  ~scoping_ptr()


  {


    delete m_ptr;


  }


  T &operator *();


  T *operator ->();


private:


  T *const m_ptr;


private:


  scoping_ptr(scoping_ptr<T> const &);


  scoping_ptr &operator =(scoping_ptr<T> const &);


};



The resources for the type are allocated and passed to the constructor, and they are uninitialized and deallocated in the destructor. During the rest of the lifetime of the instance, its contents cannot be changed. Note that hiding the copy constructor and copy-assignment operators enforces immutability (see section 2.2); m_ptr is defined to be a constant pointer just as an extra constraint for safety junkies.

Scoping classes (see Chapter 6) are generally immutable RAII types, but the concept is a bit restrictive for most uses one would wish to make of value types.

Conversely a mutable RAII type provides mechanisms so that it can be set to encapsulate another resource, or no resource at all. std::auto_ptr<> is a good example, since it provides a reset() method and assignment operators to change the managed pointer.



std::auto_ptr<int>    api(new int(1)); // Manage an int





api.reset(new int(2));  // Manage a new int





api.reset();            // Manage nothing



Mutable RAII is very useful, but it does lead to complications in the semantics of such types—an ordered sequence of any number of individual allocate/deallocate cycles within the lifetime of an instance has to be supported. At first glance, this seems like an unnecessary complication, but it is often necessary from a logical point of view. Furthermore, it can be more efficient when the managed resource is expensive to create and/or destroy, and instances of it are often unused.

3.5.2 Internally/Externally Initialized

As we just saw with the auto_ptr and scoping_ptr classes, the resource is created externally, and is passed to the constructor of RAII instance, which then assumes ownership of it. This is external initialization. The converse type is internal initialization, wherein the class is responsible for both resource acquisition and destruction (see Listing 3.9).

Listing 3.9.


class mem_buffer


{


public:


  mem_buffer(size_t size) // Allocate the buffer


    : m_size(cb)


    , m_buffer(new byte_t[size])


  {}


  ~mem_buffer()           // Release the buffer


  {


    delete [] m_buffer;


  }


public:


  operator byte_t *();    // Access the buffer


  size_t size() const;





private:


  size_t  m_size;


  byte_t  *m_buffer;


};



3.5.3 RAII Permutations

Naturally the four permutations of mutability and initialization represent different kinds of classes. For example, immutable, internally initialized types represent the purest form of RAII: easiest to code, easiest to understand, and inflexible (which can be a good thing). There's no need to worry about bad initialization values, but the constructor must take account of failure to allocate the resources. Simple utility classes often fall into this category, for example, the auto_buffer (see section 32.2).

Immutable, externally initialized types are similarly easy to code and understand, and exchange allocation failure handling for the increase in complexity by having to deal with null, and invalid, resource references. Most scoping classes (see Chapter 6) fit into this category.

The mutable types represent considerable increases in complexity over their immutable brethren. Indeed, in many cases the increase is needless, and represents one of the common ways in which C++ is abused by overeager object-oriented enthusiasts. For now, we can say that such types represent a great deal more flexibility, but have to pay the cost of more complicated implementations, dealing with copy semantics, and having to eschew useful constraints such as const/reference members (section 2.2.1). The more powerful your class, the more likely it will fall into this category.

We see examples of all these types of classes throughout the book.


      Previous section   Next section