16.5. A Generic Handle Class
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 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:
This class looks like our other handles, as does the assignment operator.
/* 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; } }
};
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 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 const versions would be similar and are left as an exercise.
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");
}
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.
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.
{ // 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
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>:
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.
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
};
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:
It's worthwhile to look in detail at the statement that calls net_price:
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;
}
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 classThe 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.
sum += (*iter)->net_price(items.count(*iter));
Exercises Section 16.5.2
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.