14.6. Member Access OperatorsTo support pointerlike classes, such as iterators, the language allows the dereference (*) and arrow (->) operators to be overloaded.
Building a Safer PointerThe dereference and arrow operators are often used in classes that implement smart pointers (Section 13.5.1, p. 495). As an example, let's assume that we want to define a class type to represent a pointer to an object of the Screen type that we wrote in Chapter 12. We'll name this class ScreenPtr. Our ScreenPtr class will be similar to our second HasPtr class. Users of ScreenPtr will be expected to pass a pointer to a dynamically allocated Screen. The ScreenPtr class will own that pointer and arrange to delete the underlying object when the last ScreenPtr referring to it goes away. In addition, we will not give our ScreenPtr class a default constructor. This way we'll know that a ScreenPtr object will always refer to a Screen. Unlike a built-in pointer, there will be no unbound ScreenPtrs. Applications can use ScreenPtr objects without first testing whether they refer to a Screen object. As does the HasPtr class, the ScreenPtr class will use-count its pointer. We'll define a companion class to hold the pointer and its associated use count:
// private class for use by ScreenPtr only
class ScrPtr {
friend class ScreenPtr;
Screen *sp;
size_t use;
ScrPtr(Screen *p): sp(p), use(1) { }
~ScrPtr() { delete sp; }
};
This class looks a lot like the U_Ptr class and has the same role. ScrPtr holds the pointer and associated use count. We make ScreenPtr a friend so that it can access the use count. The ScreenPtr class manages the use count: /* * smart pointer: Users pass to a pointer to a dynamically allocated Screen, which * is automatically destroyed when the last ScreenPtr goes away */ class ScreenPtr { public: // no default constructor: ScreenPtrs must be bound to an object ScreenPtr(Screen *p): ptr(new ScrPtr(p)) { } // copy members and increment the use count ScreenPtr(const ScreenPtr &orig): ptr(orig.ptr) { ++ptr->use; } ScreenPtr& operator=(const ScreenPtr&); // if use count goes to zero, delete the ScrPtr object ~ScreenPtr() { if (--ptr->use == 0) delete ptr; } private: ScrPtr *ptr; // points to use-counted ScrPtr class }; Because there is no default constructor, every object of type ScreenPtr must provide an initializer. The initializer must be another ScreenPtr or a pointer to a dynamically allocated Screen. The constructor allocates a new ScrPtr object to hold that pointer and an associated use count. An attempt to define a ScreenPtr with no initializer is in error: ScreenPtr p1; // error: ScreenPtr has no default constructor ScreenPtr ps(new Screen(4,4)); // ok: ps points to a copy of myScreen Supporting Pointer OperationsAmong the fundamental operations a pointer supports are dereference and arrow. We can give our class these operations as follows: class ScreenPtr { public: // constructor and copy control members as before Screen &operator*() { return *ptr->sp; } Screen *operator->() { return ptr->sp; } const Screen &operator*() const { return *ptr->sp; } const Screen *operator->() const { return ptr->sp; } private: ScrPtr *ptr; // points to use-counted ScrPtr class }; Overloading the Dereference OperatorThe dereference operator is a unary operator. In this class, it is defined as a member so it has no explicit parameters. The operator returns a reference to the Screen to which this ScreenPtr points. As with the subscript operator, we need both const and nonconst versions of the dereference operator. These differ in their return types: The const member returns a reference to const to prevent users from changing the underlying object. Overloading the Arrow OperatorOperator arrow is unusual. It may appear to be a binary operator that takes an object and a member name, dereferencing the object in order to fetch the member. Despite appearances, the arrow operator takes no explicit parameter. There is no second parameter because the right-hand operand of -> is not an expression. Rather, the right-hand operand is an identifier that corresponds to a member of a class. There is no obvious, useful way to pass an identifier as a parameter to a function. Instead, the compiler handles the work of fetching the member. When we write point->action(); precedence rules make it equivalent to writing (point->action)(); In other words, we want to call the result of evaluating point->action. The compiler evaluates this code as follows:
Using Overloaded ArrowWe can use a ScreenPtr object to access members of a Screen as follows:
ScreenPtr p(&myScreen); // copies the underlying Screen
p->display(cout);
Because p is a ScreenPtr, the meaning of p->display isthe same as evaluating (p.operator->())->display. Evaluating p.operator->() calls the operator-> from class ScreenPtr, which returns a pointer to a Screen object. That pointer is used to fetch and run the display member of the object to which the ScreenPtr points. Constraints on the Return from Overloaded Arrow
If the return type is a pointer, then the built-in arrow operator is applied to that pointer. The compiler dereferences the pointer and fetches the indicated member from the resulting object. If the type pointed to does not define that member, then the compiler generates an error. If the return value is another object of class type (or reference to such an object), then the operator is applied recursively. The compiler checks whether the type of the object returned has a member arrow and if so, applies that operator. Otherwise, the compiler generates an error. This process continues until either a pointer to an object with the indicated member is returned or some other value is returned, in which case the code is in error. |