Section 16.5. A Generic Handle Class


16.5. A Generic Handle Class

This example represents a fairly sophisticated use of C++. Understanding it requires understanding both inheritance and templates fairly well. It may be useful to delay studying this example until you are comfortable with these features. On the other hand, this example provides a good test of your understanding of these features.



In Chapter 15 we defined two handle classes: the Sales_item (Section 15.8, p. 598) class and the Query (Section 15.9, p. 607) class. These classes managed pointers to objects in an inheritance hierarchy. Users of the handle did not have to manage the pointers to those objects. User code was written in terms of the handle class. The handle dynamically allocated and freed objects of the related inheritance classes and forwarded all "real" work back to the classes in the underlying inheritance hierarchy.

These handles were similar to but different from each other: They were similar in that each defined use-counted copy control to manage a pointer to an object of a type in an inheritance hierarchy. They differed with respect to the interface they provided to users of the inheritance hierarchy.

The use-counting implementation was the same in both classes. This kind of problem is well suited to generic programming: We could define a class template to manage a pointer and do the use-counting. Our otherwise unrelated Sales_item and Query types could be simplified by using that template to do the common use-counting work. The handles would remain different as to whether they reveal or hide the underlying inheritance hierarchy.

In this section, we'll implement a generic handle class to provide the operations that manage the use count and the underlying objects. Then we'll rewrite the Sales_item class, showing how it could use the generic handle rather than defining its own use-counting operations.

16.5.1. Defining the Handle Class

Our Handle class will behave like a pointer: Copying a Handle will not copy the underlying object. After the copy, both Handles will refer to the same underlying object. To create a Handle, a user will be expected to pass the address of a dynamically allocated object of the type (or a type derived from that type) managed by the Handle. From that point on, the Handle will "own" the given object. In particular, the Handle class will assume responsibility for deleting that object once there are no longer any Handles attached to it.

Given this design, here is an implementation of our generic Handle:

      /* generic handle class: Provides pointerlike behavior. Although access through       * an unbound Handle is checked and throws a runtime_error exception.       * The object to which the Handle points is deleted when the last Handle goes away.       * Users should allocate new objects of type T and bind them to a Handle.       * Once an object is bound to a Handle,, the user must not delete that object.       */      template <class T> class Handle {      public:          // unbound handle          Handle(T *p = 0): ptr(p), use(new size_t(1)) { }          // overloaded operators to support pointer behavior          T& operator*();          T* operator->();          const T& operator*() const;          const T* operator->() const;          // copy control: normal pointer behavior, but last Handle deletes the object          Handle(const Handle& h): ptr(h.ptr), use(h.use)                                              { ++*use; }          Handle& operator=(const Handle&);          ~Handle() { rem_ref(); }      private:          T* ptr;          // shared object          size_t *use;     // count of how many Handle spointto *ptr          void rem_ref()              { if (--*use == 0) { delete ptr; delete use; } }      }; 

This class looks like our other handles, as does the assignment operator.

      template <class T>      inline Handle<T>& Handle<T>::operator=(const Handle &rhs)      {          ++*rhs.use;      // protect against self-assignment          rem_ref();       // decrement use count and delete pointers if needed          ptr = rhs.ptr;          use = rhs.use;          return *this;      } 

The only other members our class will define are the dereference and member access operators. These operators will be used to access the underlying object. We'll provide a measure of safety by having these operations check that the Handle is actually bound to an object. If not, an attempt to access the object will throw an exception.

The nonconst versions of these operators look like:

      template <class T> inline T& Handle<T>::operator*()      {          if (ptr) return *ptr;          throw std::runtime_error                         ("dereference of unbound Handle");      }      template <class T> inline T* Handle<T>::operator->()      {          if (ptr) return ptr;          throw std::runtime_error                         ("access through unbound Handle");      } 

The const versions would be similar and are left as an exercise.

16.5.2. Using the Handle

We intend this class to be used by other classes in their internal implementations. However, as an aid to understanding how the Handle class works, we'll look at a simpler example first. This example illustrates the behavior of the Handle by allocating an int and binding a Handle to that newly allocated object:

Exercises Section 16.5.1

Exercise 16.45:

Implement your own version of the Handle class.

Exercise 16.46:

Explain what happens when an object of type Handle is copied.

Exercise 16.47:

What, if any, restrictions does Handle place on the types used to instantiate an actual Handle class.

Exercise 16.48:

Explain what happens if the user attaches a Handle to a local object. Explain what happens if the user deletes the object to which a Handle is attached.


      { // new scope        // user allocates but must not delete the object to which the Handle is attached        Handle<int> hp(new int(42));        { // new scope            Handle<int> hp2 = hp; // copies pointer; use count incremented            cout << *hp << " " << *hp2 << endl; // prints 42 42            *hp2 = 10;           // changes value of shared underlying int        }   // hp2 goes out of scope; use count is decremented        cout << *hp << endl; // prints 10      } // hp goes out of scope; its destructor deletes the int 

Even though the user of Handle allocates the int, the Handle destructor will delete it. In this code, the int is deleted at the end of the outer block when the last Handle goes out of scope. To access the underlying object, we apply the Handle * operator. That operator returns a reference to the underlying int object.

Using a Handle to Use-Count a Pointer

As an example of using Handle in a class implementation, we might reimplement our Sales_item class (Section 15.8.1, p. 599). This version of the class defines the same interface, but we can eliminate the copy-control members by replacing the pointer to Item_base by a Handle<Item_base>:

      class Sales_item {      public:          // default constructor: unbound handle          Sales_item(): h() { }          // copy item and attach handle to the copy          Sales_item(const Item_base &item): h(item.clone()) { }          // no copy control members: synthesized versions work          // member access operators: forward their work to the Handle class          const Item_base& operator*() const { return *h; }          const Item_base* operator->() const                                 { return h.operator->(); }      private:          Handle<Item_base> h; // use-counted handle      }; 

Although the interface to the class is unchanged, its implementation differs considerably from the original:

  • Both classes define a default constructor and a constructor that takes a const reference to an Item_base object.

  • Both define overloaded * and -> operators as const members.

The Handle-based version of Sales_item has a single data member. That data member is a Handle attached to a copy of the Item_base object given to the constructor. Because this version of Sales_item has no pointer members, there is no need for copy-control members. This version of Sales_item can safely use the synthesized copy-control members. The work of managing the use-count and associated Item_base object is done inside Handle.

Because the interface is unchanged, there is no need to change code that uses Sales_item. For example, the program we wrote in Section 15.8.3 (p. 603) can be used without change:

      double Basket::total() const      {          double sum = 0.0; // holds the running total          /* find each set of items with the same isbn and calculate           * the net price for that quantity of items           * iter refers to first copy of each book in the set           * upper_boundrefers to next element with a different isbn           */          for (const_iter iter = items.begin();                          iter != items.end();                          iter = items.upper_bound(*iter))          {              // we know there's at least one element with this key in the Basket              // virtual call to net_priceapplies appropriate discounts, if any              sum += (*iter)->net_price(items.count(*iter));          }          return sum;      } 

It's worthwhile to look in detail at the statement that calls net_price:

      sum += (*iter)->net_price(items.count(*iter)); 

This statement uses operator -> to fetch and run the net_price function. What's important to understand is how this operator works:

  • (*iter) returns h, our use-counted handle member.

  • (*iter)-> therefore uses the overloaded arrow operator of the handle class

  • The compiler evaluates h.operator->(), which in turn yields the pointer to Item_base that the Handle holds.

  • The compiler dereferences that Item_base pointer and calls the net_price member for the object to which the pointer points.

Exercises Section 16.5.2

Exercise 16.49:

Implement the version of the Sales_item handle presented here that uses the generic Handle class to manage the pointer to Item_base.

Exercise 16.50:

Rerun the function to total a sale. List all changes you had to make to get your code to work.

Exercise 16.51:

Rewrite the Query class from Section 15.9.4 (p. 613) to use the generic Handle class. Note that you will need to make the Handle a friend of the Query_base class to let it access the Query_base destructor. List and explain all other changes you made to get the programs to work.




C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

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