20.1 Holders and Trules

Ru-Brd

20.1 Holders and Trules

This section introduces two smart pointer types: a holder type to hold an object exclusively and a so-called trule to enable the transfer of ownership from one holder to another.

20.1.1 Protecting Against Exceptions

Exceptions were introduced in C++ to improve the reliability of C++ programs. They allow regular and exceptional execution paths to be more cleanly separated. Yet shortly after exceptions were introduced, various C++ programming authors and columnists started observing that a naive use of exceptions leads to trouble, and particularly to memory leaks. The following example shows but one of the many troublesome situations that could arise:

 void do_something()  {      Something* ptr = new Something;  // perform some computation with  *ptr      ptr->perform();   delete ptr;  } 

This function creates an object with new , performs some operations with this object, and destroys the object at the end of the function with delete . Unfortunately, if something goes wrong after the creation but before the deletion of the object and an exception gets thrown, the object is not deallocated and the program leaks memory. Other problems may arise because the destructor is not called (for example, buffers may not be written out to disk, network connections may not be released, on-screen windows may not be closed, and so forth). This particular case can be handled fairly easily using an explicit exception handler:

 void do_something()  {      Something* ptr = 0;      try {          ptr = new Something;  // perform some computation with  *ptr          ptr->perform();   }      catch (...) {          delete ptr;          throw;  // rethrow the exception that was caught  }      return result;  } 

This is manageable, but already we find that the exceptional path is starting to dominate the regular path, and the deletion of the object has to be done in two different places: once in the regular path and once in the exceptional path . This avenue quickly grows worse . Consider what happens if we need to create two objects in a single function:

 void do_two_things()  {      Something* first = new Something;      first->perform();      Something* second = new Something;      second->perform();      delete second;      delete first;  } 

Using an explicit exception handler, there are various ways to make this exception-safe, but none seems very appealing. Here is one option:

 void do_two_things()  {      Something* first = 0;      Something* second = 0;      try {          first = new Something;          first->perform();          second = new Something;          second->perform();      }      catch (...) {          delete first;          delete second;          throw;  // rethrow the exception that was caught  }      delete second;      delete first;  } 

Here we made the assumption that the delete operations will not themselves trigger exceptions. [1] In this example, the exception handling code is a very large part of the routine, but more important, it could be argued that it is the most subtle part of it. The need for exception safety has also significantly changed the structure of the regular path of our routine ”perhaps more so than you may feel comfortable with.

[1] This is a reasonable assumption. Destructors that throw exceptions should generally be avoided because destructors are automatically called when an exception is thrown, and throwing another exception while this happens results in immediate program termination.

20.1.2 Holders

Fortunately, it is not very hard to write a small class template that essentially encapsulates the policy in the second example. The idea is to write a class that behaves most like a pointer, but which destroys the object to which it points if it is itself destroyed or if another pointer is assigned to it. Such a class could be called a holder because it is meant to hold an object safely while we perform various computations . Here is how we could do this:

  // pointers/holder.hpp  template <typename T>  class Holder {    private:      T* ptr;  // refers to the object it holds (if any)  public:  // default constructor: let the holder refer to nothing  Holder() : ptr(0) {      }  // constructor for a pointer: let the holder refer to where the pointer refers  explicit Holder (T* p) : ptr(p) {      }  // destructor: releases the object to which it refers (if any)  ~Holder() {          delete ptr;      }  // assignment of new pointer  Holder<T>& operator= (T* p) {          delete ptr;          ptr = p;          return *this;      }  // pointer operators  T& operator* () const {          return *ptr;      }      T* operator-> () const {          return ptr;      }  // get referenced object (if any)  T* get() const {          return ptr;      }  // release ownership of referenced object  void release() {          ptr = 0;      }  // exchange ownership with other holder  void exchange_with (Holder<T>& h) {          swap(ptr,h.ptr);      }  // exchange ownership with other pointer  void exchange_with (T*& p) {          swap(ptr,p);      }    private:  // no copying and copy assignment allowed  Holder (Holder<T> const&);      Holder<T>& operator= (Holder<T> const&);  }; 

Semantically, the holder takes ownership of the object to which ptr refers. This object has to be created with new , because delete is used whenever the object owned by the holder has to be destroyed. [2] The release() member removes control over the held object from the holder. However, the plain assignment operator is smart enough to destroy and deallocate any object held because another object will be held instead and the assignment operator does not return a holder or pointer for the original object. We added two exchange_with() members that allow us to replace conveniently the object being held without destroying it.

[2] A template parameter defining a deallocation policy could be added to improve flexibility in this area.

Our example with two allocations can be rewritten as follows :

 void do_two_things()  {      Holder<Something> first(new Something);      first->perform();      Holder<Something> second(new Something);      second->perform();  } 

This is much cleaner. Not only is the code exception-safe because of the work done by the Holder destructors, but the deletion is also automatically done when the function terminates through its regular path (at which point the objects indeed were to be destroyed).

Note that you can't use the assignment-like syntax for initialization:

 Holder<Something> first = new Something;  // ERROR  

This is because the constructor is declared as explicit and there is a minor difference between

 X x;  Y y(x);  // explicit conversion  

and

 X x;  Y y = x;  // implicit conversion  

The former creates a new object of type Y by using an explicit conversion from type X , whereas the latter creates a new object of type Y by using an implicit conversion, but in our case implicit conversions are inhibited by the keyword explicit .

20.1.3 Holders as Members

We can also avoid resource leaks by using holders within a class. When a member has a holder type instead of an ordinary pointer type, we often no longer need to deal explicitly with that member in the destructor because the object to which it refers gets deleted with the deletion of the holder member. In addition, a holder helps to avoid resource leaks that are caused by exceptions that are thrown during the initialization of an object. Note that destructors are called only for those objects that are completely constructed . So, if an exception occurs inside a constructor, destructors are called only for member objects with a constructor that finished normally. Without holders, this may result in a resource leak if, for example, a first successful allocation was followed by an unsuccessful one. For example:

  // pointers/refmem1.hpp  class RefMembers {    private:      MemType* ptr1;  // referenced members  MemType* ptr2;    public:  // default constructor   // - will cause resource leak if second  new  throws  RefMembers ()       : ptr1(new MemType), ptr2(new MemType) {      }  // copy constructor   // - might cause resource leak if second  new  throws  RefMembers (RefMembers const& x)       : ptr1(new MemType(*x.ptr1)), ptr2(new MemType(*x.ptr2)) {      }  // assignment operator  const RefMembers& operator= (RefMembers const& x) {         *ptr1 = *x.ptr1;         *ptr2 = *x.ptr2;         return *this;      }      ~RefMembers () {          delete ptr1;          delete ptr2;      }   }; 

By using holders instead of ordinary pointer members, we easily avoid these potential resource leaks:

  // pointers/refmem2.hpp  #include "holder.hpp"  class RefMembers {    private:      Holder<MemType> ptr1;  // referenced members  Holder<MemType> ptr2;    public:  // default constructor   // - no resource leak possible  RefMembers ()       : ptr1(new MemType), ptr2(new MemType) {      }  // copy constructor   // - no resource leak possible  RefMembers (RefMembers const& x)       : ptr1(new MemType(*x.ptr1)), ptr2(new MemType(*x.ptr2)) {      }  // assignment operator  const RefMembers& operator= (RefMembers const& x) {         *ptr1 = *x.ptr1;         *ptr2 = *x.ptr2;         return *this;      }  // no destructor necessary   // (default destructor lets  ptr1  and  ptr2  delete their objects)    }; 

Note that although we can now omit a user -defined destructor, we still have to program the copy constructor and the assignment operator.

20.1.4 Resource Acquisition Is Initialization

The general idea supported by holders is a pattern called resource acquisition is initialization or just RAII , which was introduced in [StroustrupDnE]. By introducing template parameters for deallocation policies, we can replace all code that matches the following outline:

 void do()  {  // acquire resources  RES1* res1 = acquire_resource_1();      RES2* res2 = acquire_resource_2();    // release resources  release_resource_2(res);      release_resource_1(res);  } 

with

 void do()  {  // acquire resources  Holder<RES1,   > res1(acquire_resource_1());      Holder<RES2,   > res2(acquire_resource_2());   } 

This can be done by something similar to our uses of Holder , with the added advantage that the code is exception-safe.

20.1.5 Holder Limitations

Not every problem is resolved with our implementation of the Holder template. Consider the following example:

 Something* load_something()  {      Something* result = new Something;      read_something(result);      return result;  } 

In this example, two things make the code more complicated:

  1. Inside the function, read_something() , which is a function that expects an ordinary pointer as its argument, is called.

  2. load_something() returns an ordinary pointer.

Now, using a holder, the code becomes exception-safe but more complicated:

 Something* load_something()  {      Holder<Something> result(new Something);      read_something(result.get_pointer());      Something* ret = result.get_pointer();      result.release();      return ret;  } 

Presumably, the function read_something() is not aware of the Holder type; hence we must extract the real pointer using the member function get_pointer() . By using this member function, the holder keeps control over the object, and the recipient of the result of the function call should understand that it does not own the object whose pointer it gets ”the holder does.

If no get_pointer() member function is provided, we can also use the user-defined indirection operator * , followed by the built-in address-of operator & . Yet another alternative is to call operator -> explicitly. The following example illustrates this:

 read_something(&*result);  read_something(result.operator->()); 

You'll probably agree that the latter is a particularly ugly alternative. However, it may be appropriate to attract the attention to the fact that something relatively dangerous is being done.

Another issue in the example code is that we must call release() to cancel the ownership of the object being referred to. This prevents that object from being destroyed when the function is done; hence it can be returned to the caller. Note that we must store the return value in a temporary variable before releasing it:

 Something* ret = result.get_pointer();  result.release();  return ret; 

To avoid this, we can enable statements such as

 return result,release(); 

by modifying release() so that it returns the object previously owned:

 template <typename T>  class Holder {   T* release() {          T* ret = ptr;          ptr = 0;          return ret;      }   }; 

This leads to an important observation: Smart pointers are not that smart, but used with a simple consistent policy they do make life much simpler.

20.1.6 Copying Holders

You probably noticed that in our implementation of the Holder template we disabled copying of holders by making the copy constructor and the copy-assignment operator private. Indeed, the purpose of copying is usually to obtain a second object that is essentially identical to the original. For a holder this would mean that the copy also thinks it controls when the object gets deallocated, and chaos ensues because both holders are inclined to deallocate the controlled object. Thus, copying is not an appropriate operation for holders. Instead, we can conceive of transfer as being the natural counterpart of copying in this case.

A transfer operation is fairly easily achieved using a release operation followed by initialization or assignment, as shown in the following:

 Holder<Something> h1(new Something);  Holder<Something> h2(h1.release()); 

Note again that the syntax

 Holder<X> h = p; 

will not work because it implies an implicit conversion whereas the constructor is declared as explicit :

 Holder<Something> h2 = h1.release();  // ERROR  

20.1.7 Copying Holders Across Function Calls

The explicit transfer works well, but the situation is a little more subtle when the transfer is across a function call. For the case of passing a holder from a caller to a callee, we can always pass by reference instead of passing by value. Using the "release followed by initialization" approach can lead to problems when more than one argument is passed:

 MyClass x;  callee(h1.release(),x);  // passing  x  may throw!  

If the compiler chooses first to cause h1.release() to be evaluated, then the subsequent copying of x ( assuming it is passed by value) may trigger an exception that occurs, whereas no component is in charge of releasing the object that used to be owned by holder h1 . Hence, a holder should always be passed by reference.

Unfortunately, it is in general not convenient to return a holder by reference because this requires the holder to have a lifetime that exceeds the function call, which in turn makes it unclear when and how the holder will deallocate the object under its control. You can build an argument that it is fine to call release() on a holder just prior to returning the encapsulated pointer. This is essentially what we did with load_something() earlier. Consider the following situation:

 Something* creator()  {      Holder<Something> h(new Something);      MyClass x;  // for illustration purposes  return h.release();  } 

We must be aware here that the destruction of x could cause an exception to be thrown after h has released the object it owned and before that object was placed under the control of another entity. If so, we would again have a resource leak. (Allowing exceptions to escape from destructors is rarely a good idea: It makes it easy for an exception to be thrown while the call stack is being unwound for a previous exception, and this leads to immediate termination of the program. The latter situation can be guarded against, but it makes for harder to understand ”and therefore more brittle ”code.)

20.1.8 Trules

To solve such problems let's introduce a helper class template dedicated to transferring holders. We call this class template a trule , which is a term derived from the contraction of transfer capsule . Here is its definition:

  // pointers/trule.hpp  #ifndef TRULE_HPP  #define TRULE_HPP  template <typename T>  class Holder;  template <typename T>  class Trule {    private:      T* ptr;  // objects to which the trule refers (if any)  public:  // constructor to ensure that a trule is used only as a return type   // to transfer holders from callee to caller!  Trule (Holder<T>& h) {          ptr = h.get();          h.release();      }  // copy constructor  Trule (Trule<T> const& t) {          ptr = t.ptr;          const_cast<Trule<T>&>(t).ptr = 0;      }  // destructor  ~Trule() {          delete ptr;      }    private:      Trule(Trule<T>&);  // discourage use of lvalue trules  Trule<T>& operator= (Trule<T>&);  // discourage copy assignment  friend class Holder<T>;  };  #endif  // TRULE_HPP  

Clearly, something ugly is going on in the copy constructor. Because transfer capsules are meant as the return type of functions that wish to transfer holders, they always occur as temporary objects (rvalues); hence they can be bound only to reference-to- const types. However, the transfer cannot just be a copy and must remove the ownership, by nulling the encapsulated pointer, from the original Trule . The latter operation is intrinsically non- const . This state of affairs is ugly, but it is in fact legal to cast away constness in these cases because the original object was not declared const . Hence, we must be careful to declare the return type of a function transferring a holder as Trule<T> and not Trule<T> const .

Note that no such code is used for converting a holder into a trule: The holder must be a modifiable lvalue. This is why we use a separate type for the transfer capsule instead of merging this functionality into the Holder class template.

To discourage the use of Trule as anything but a return type for transferring holders, a copy constructor taking a reference to a non- const object and a similar copy-assignment operator were declared private. This prevents us from doing much with lvalue Trule s, but it is only a very partial measure. The goal of a trule is to help the responsible software engineer, not to thwart the mad scientist.

The Trule template is not complete until it is recognized by the Holder template:

  // pointers/holder2extr.hpp  template <typename T>  class Holder {  // previously defined members    public:      Holder (Trule<T> const& t) {          ptr = t.ptr;          const_cast<Trule<T>&>(t).ptr = 0;      }      Holder<T>& operator= (Trule<T> const& t) {          delete ptr;          ptr = t.ptr;          const_cast<Trule<T>&>(t).ptr = 0;          return *this;      }  }; 

To illustrate this refined Holder / Trule pair, we can rewrite our load_something() example and invent a caller for it:

  // pointers/truletest.cpp  #include "holder2.hpp"  #include "trule.hpp"  class Something {  };  void read_something (Something* x)  {  }  Trule<Something> load_something()  {      Holder<Something> result(new Something);      read_something(result.get());      return result;  }  int main()  {      Holder<Something> ptr(load_something());   } 

To conclude, we have created a pair of class templates that are almost as convenient to use as plain pointers with the added benefit of managing the deallocation of objects necessary when the stack gets unwound as the result of an exception being thrown.

Ru-Brd


C++ Templates
C++ Templates: The Complete Guide
ISBN: 0201734842
EAN: 2147483647
Year: 2002
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net