16.5. A Generic Handle Class
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 ClassOur 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 HandleWe 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: { // 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 PointerAs 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:
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:
![]() |