Previous section   Next section

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


3.4. RRID Types

The previous mechanisms are the domain of the C programmer [Lind1994]. Even where we've used C++ to help us out with wrapper proxies, the encapsulation was an enhancement to usability. When we need to manage the lifetimes of resources, C++ offers programmers better options.

While I was explaining the veneer (see Chapter 21) class sequence_container_veneer, which I'll discuss in a moment, to my friend Scott Patterson, he observed that what I was describing didn't really fit the RAII (see section 3.5) idiom, since there was no acquisitional component to the class—just release upon destruction. He then suggested the derivative term Resource Release Is Destruction (RRID). I like it, if for no better reason than that it is just as hard to say or to understand as its more complete elder brother.[3]

[3] Personally, I think they should both change the Is to At, and they'd make a lot more sense, but they're not my phrases.

Definition: Resource Release Is Destruction is a mechanism that takes advantage of C++'s support for automatic destruction to ensure the deterministic release of resources associated with an instance of an encapsulating type.


Despite RRID not having an acquisitional component, it can still be most useful. This is because it is the destruction of an object, and the release of its allocated resource(s), that can be lost in premature or inappropriate exiting of a scope and that we therefore wish to bind into the compiler's clean-up infrastructure.



{


  int device;





  if( . . . ) // Some selection criteria


  {


    device = open(. . .);


  }


  else


  {


    device = socket(. . .);


  }





  . . . // If you return, or throw exception, here then device remains open





  close(device);


}



Before an object is constructed there is nothing to lose, so there is no need to automate the construction.

So RRID types are characterized as providing built-in destruction semantics. However, it's not just a simple case of omitting all initialization; a destructor acting upon uninitialized data would exhibit undefined behavior, which means it will mostly likely crash in your first important demo—you know, the one your employer is relying on to bring in the contract that'll keep the company afloat. No pressure, or anything.

The presence or absence of (default) initialization characterizes the two flavors of RRID discussed in the following subsections.

3.4.1 Default-Initialized: Lazy Initialization

The default-initialized form of RRID involves setting the resource reference in the encapsulating type to some null value that will result in correct destruction behavior, usually to do nothing at all. The minimal form of a default-initialized RRID type would look like the following:



struct DeviceCloser


{


  DeviceCloser() // Default initializtion


    : m_device(NULL)


  {}


  ~DeviceCloser() // Destruction


  {


    close(m_device); // Close the device handle


  }


  int m_device;


};



This can be inserted into the code shown above, and will plug the memory leak. If you wish, you can make device a reference to the DeviceCloser instance's pointer, to minimize the code changes:



{


  DeviceCloser  dc;


  int           *&device = dc.m_file;





  if( . . . ) // Some selection criteria


  {


    device = open(. . .);


  }


  else


  {


    device = socket(. . .);


  }


  . . . // Whatever happens here, the device will be closed


} // Device closed here



DeviceCloser has a constructor that initializes its m_device member to a valid state, but it acquires no resources, so it's not RAII (see section 3.5). (I've made it a struct because all members are public.)

Naturally, it's preferable to initialize the resource handler in its constructor, but this is not always possible, or convenient at least. Consider the case where we might need to perform some other actions on the conditional branch where we open the device from a socket. These other actions might cause an exception to be thrown. One option is to move all that branch's code out into a separate function which itself will either return the opened socket, or will catch the thrown exception, close the socked handle, and rethrow the exception. But that's not necessarily always preferable.

Classes such as DeviceCloser can be useful (generalized in template form, of course), but in the majority of cases we're better served by a higher level of encapsulation, as we'll see shortly.

3.4.2 Uninitialized

The second form of RRID is where there is no initialization performed at all. Naturally this is quite dangerous, so this form is of very limited appeal. Because no initialization is performed, the costs of initialization are avoided. However, this is only valid when the initialization that would be performed in the client code is guaranteed to happen. If it's not, then an execution path that leads to the destructor of the RRID instance before it's been set to a valid state will cause a crash. This is so rarely the case that this technique should have a big red warning sign over it: "Bridge out!" There is only one case where I've ever used it, which I'll describe in Chapter 21, when we look at veneers. In that case it is in the form of the pod_veneer template, which takes two policy classes that stipulate initialization and uninitialization semantics for a given parameterization of the template.

Having said all that, there's a twist on this technique when it concerns the layering of another type around an existing type whose resources are well managed. In such cases it is not dangerous at all, and therefore eminently usable.

One thing that has always irked me about the standard library's container classes is that they do not clean up after themselves properly. Actually, I am not being fair; they clean up after themselves perfectly insofar as they correctly control the lifetime of the instances that they manage. The problem is that when the instances that they manage are pointers, they don't do anything with the pointed-to objects, just the pointers, and the destructor for a pointer is the same as it is for any nonclass type: a no-op.

Of course, such a fantastic blend of genericity and efficiency could not account for such special cases, but it is vexing nonetheless. This is especially so when one cannot use certain lifetime-managing classes in the containers. Unless the pointed-to object is of a type that is reference-counted, or can be adapted to be reference-counted,[4] we are left with manipulating the container contents as raw pointers.

[4] The Boost shared_ptr can be used to provide nonintrusive reference-counting.

Despite the portents, this can actually be fine in many cases, and when explicitly removing elements from a container, it is not too onerous to remember to destroy the element once its pointer has been erased from the container. This can always be simplified with a specific method on the class using the container, as shown in Listing 3.5:

Listing 3.5.


class Resource;





class ResourceManager


{


  . . .


// Members


private:


  typedef std::vector<Resource*> Resources_t;


  Resources_t m_resources;


};





void ResourceManager::EraseResource(size_t index)


{


  delete m_resources[index];


  m_resources.erase(&m_resources[index]);


}



The one issue that is hard to deal with in this manner is the erasure of elements from the container in its destructor, which will leave all still pointed-to elements dangling in hyperspace: a memory (and who knows what else) leak! To avoid this, the author of ResourceManager would have to include the following code in the destructor for that class.



void del_Resource(Resource *);





void ResourceManager::~ResourceManager()


{


  std::for_each(m_resources.begin(),m_resources.end(),


                del_Resource);


}



This doesn't seem too bad, but this is a trivial example, after all. I've seen examples of this in the real world involving up to ten managing containers within a single class. Putting aside the issue of whether such coding indicates a design problem, this is a maintainer's nightmare, and is virtually guaranteed to end in unhappiness.

The sequence_container_veneer is a template that takes as its parameters the container type to be wrapped, and the type of a functor that will be used to clean up the outstanding resources (see Listing 3.6).

Listing 3.6.


template< typename C


        , typename F


        >


class sequence_container_veneer


  : public C


{


public:


  ~sequence_container_veneer()


  {


    // delete all remaining elements with F()


    std::for_each(begin(), end(), F());


  }


  . . .


};



The only method it defines is the destructor; hence it is pure RRID. It relies on its parameterizing sequence type, from which it derives, being correctly constructed itself. A simple change to the design of ResourceManager can relieve the maintenance developer from some headache, as shown in Listing 3.7:

Listing 3.7.


struct RelResource


{


  void operator ()(Resource *r)


  {


    del_Resource(r);


  }


};





class ResourceManager


{





// Members


private:


  typedef std::vector<Resource*>     Resources_t;


  typedef sequence_container_veneer< Resources_t


                                   , RelResource


                                   >


                                     Resource_vec_rrid_t;





  Resource_vec_rrid_t m_resources;


};





void ResourceManager::~ResourceManager()


{


  // Nothing to do now


}



Now the container automatically does what you would want in the destructor, saving you from the manual and error prone boilerplate. We've been able to leverage the automatic destruction support provided by C++ of the parameterized sequence_container_veneer class to effect desired behavior, rather than having to create a specific class to do our task.

It is important to note that the destruction of the outstanding Resource instances is as a result of parameterizing with the RelResource type, rather than an artifact of the sequence_container_veneer class per se. A useful alternative scenario would be where the Resource instance lifetimes are managed elsewhere in the application with ResourceManager fulfilling a dispenser function, and a log trace is required when it terminates in order to determine which resources have not been used by the application logic. Indeed, anything you can think of to do with an outstanding Resource can be applied during the destruction of ResourceManager via the sequence_container_veneer template.

Before we finish this topic, I'd like to point out that if you want to use a standard library container to own resources, rather than just refer to them, there are sometimes better approaches than using a component such as sequence_container_veneer, depending on your needs. For example, if your objects are reference counted, then you can use a reference-counting class to manage them. Alternatively, you may be able to apply external reference counts to these objects.


      Previous section   Next section