12.7 SMART POINTERS: OVERLOADING OF DEREFERENCING OPERATORS


12.7 SMART POINTERS: OVERLOADING OF DEREFERENCING OPERATORS

We will now show how by overloading the dereferencing operators ‘->’ and the ‘*’, you can program up a smart pointer class whose objects can serve as smart proxies for regular pointers.[4] But first a few comments about the shortcomings of regular pointers are in order.

      Given a class X       class X {       //...       }; 

we can construct an object of this class by invoking its constructor via the new operator

         X* p = new X( ... ); 

We can dereference the pointer by calling *p, which retrieves the object for us, or we can dereference the pointer by p->some_member, which first retrieves the object and then gives us access to the member some_member of class X.

From the standpoint of memory management, there is a major shortcoming to using a pointer as a handle to a new object in the manner shown above. When the pointer goes out of scope, only the memory assigned to the pointer variable—usually four bytes—is freed up. The memory occupied by the object to which the pointer is pointing is not freed up unless you invoke the operator delete on the pointer before it goes out of scope.

What this means is that you just have to remember to invoke delete somewhere for every new (and delete [] somewhere for every new []). In a majority of situations, this programming rule of thumb works just fine to prevent memory leaks. But there can be special circumstances where this rule of thumb cannot be applied so easily. Consider the following example:

 
//PretendGiant.cc class Giant{} class Big {}; class MyClass { Giant* giant; Big* big; public: MyClass() :giant(new Giant(), //(A) big(new Big()) //(B) {} ~MyClass() delete giant; delete big; } }; int main() { MyClass myobject; //(C) return 0; }

When myobject in main goes out of scope, its destructor will be invoked, which would free up the memory occupied by the objects pointed to by the pointer data members giant and big.

Now let's pretend that objects of type Giant and Big occupy huge amounts of memory and that after the execution of the statement in line (A), there is not enough memory left over for the construction in line (B). This could cause an exception to be thrown, which would halt any further construction of the object in line (C). In the following version of the program, we have simulated this condition by causing the constructor of Big to explicitly throw an exception in line (D):

 
//ConstructorLeak . cc class Err{}; class Giant{}; class Big { public: Big()throw( Err) { throw Err(); } //(D) }; class MyClass { Giant* giant; Big* big; public: MyClass(): giant (new Giant()), big(new Big()) {} //(E) ~MyClass() { delete giant; delete big; } }; int main() { try { MyClass myobject; //(F) } catch (Err) {} return 0; }

Now the destructor of myobject will never be invoked when this object goes out of scope at the end of the try block in main. But note that the exception was thrown during the construction of the Big object in line (E)—but that was after the Giant object was already constructed. Since the destructor of MyClass is never invoked in this program, the memory occupied by the Giant object would never get released—causing a "giant" memory leak in the program.

One way to eliminate such potential memory leaks is by using a smart pointer instead of a regular pointer. When a smart pointer goes out of scope, it takes care of cleaning up after itself—in the sense that its destructor is invoked to free up the memory occupied by the object to which the pointer points.

Here is a rudimentary version of a smart pointer class, SmartPtr, whose objects can serve as smart pointers to objects of type X. We overload the dereferencing operators to make an object of type SmartPtr behave like a regular pointer.

      class SmartPtr {          X* ptr;      public:          //constructor          SmartPtr( X* p) : ptr(p) {};          // overloading of *          X&operator*() { return *ptr; }                //(G)          // overloading of ->          X* operator->() { return ptr; }               //(H)          //....      }; 

An object of type SmartPtr is simply a wrapper around an object of type X*. The overload definitions for the ‘*’ and the ‘->’ operators in lines (G) and (H) give an object of type SmartPtr the same functionality as possessed by a regular pointer. To explain, consider the following example. Let's say we have

      X* s = new X( .... ); 

We can now construct a SmartPtr for the newly created object by

      SmartPtr smart_p(s); 

Due to the overloading defined in the SmartPtr class for the member access operator ‘->,’ we may now access a member of the X object constructed by

      smart_p->some_member;                              //(I) 

When the compiler sees this construct, it first checks whether smart_p is a pointer to an object of some class (as that is the usual situation with an identifier on the left of the member-access arrow operator). In our case, that is not true. The compiler then checks whether the identifier on the left of the arrow is an object of some class for which the member access operator is overloaded. For the statement in line (I), that is indeed the case. The compiler then examines the overload definition for the ‘->’ operator in line (H) above. If the return type specified in this definition is a pointer to a class type, the semantics for the built-in member access operator are then applied to the returned value.[5] For the example at hand, the compiler will extract from the overload definition of ‘->’ the object returned by

      smart_p-> 

which is the value stored in the data member ptr of the smart_p object. That turns out to be a regular pointer to the X object to which s points. The compiler now applies the same semantics to the pointer thus obtained and whatever is on right side of the member-access arrow operator in line (I) above as it does to the regular usage of this operator.

With regard to the overload definitions, both ‘*’ and ‘->’ are unary operators. Additionally, while the former is right-associative, the latter is left-associative. It is for this reason that the compiler tries to interpret

      smart_p-> 

in line (H) before examining the object on the right of the arrow.

The overload definition for * in line (G) permits a SmartPtr object to be dereferenced in exactly the same manner as a regular pointer:

      X* p = new X( .... );      SmartPtr s(p);      cout << *s; 

The call *s returns *p and, if the output stream operator is overloaded for class X, the last statement would print out the X object constructed.

So far we have discussed how we may define a smart pointer, but we have not put any smarts into the example we showed above. We will now make the class SmartPtr "smart" by giving it the following functionality: Every time a pointer of type SmartPtr goes out of scope, we want the object pointed by its data member ptr to be deleted automatically. This can be achieved by defining the following destructor for SmartPtr:

      SmartPtr::~SmartPtr() { delete ptr; } 

Pulling together the definitions shown above, we can write the following program that is our first demonstration of the working of a smart pointer:

 
//SmartPtrInitial.cc #include <iostream> using namespace std; class X {}; class SmartPtr { X* ptr; public: SmartPtr(X* p) : ptr(p) {}; X&operator*() { return *ptr; } X* operator->() {return ptr; } ~SmartPtr() { delete ptr; cout << "Memory pointed to by ptr freed up" << endl; } }; int main() { X* xp = new X(); SmartPtr s(xp); return 0; }

The SmartPtr class shown above is only minimally functional as a replacement for pointers of type X*. Used as such, it could inject dangerous bugs into a program. Suppose we initialize a new SmartPtr object with an existing SmartPtr object:

      int main()      {          X* xp = new X();          SmartPtr s1(xp);          SmartPtr s2 = s1;      } 

With no copy constructor provided, the system will use the default definition of copy construction for initializing the object s2 from the object s1. This will cause ptr of s2 to point to the same X object as the ptr of s1. So when s1 and s2 go out of scope, their destructors will try to delete the same X object—with unpredictable results, possibly a system crash. So we must provide SmartPtr with an appropriate copy constructor. Very similar reasoning will also convince us that we must provide SmartPtr with a copy assignment operator.

Defining a copy constructor and a copy assignment operator for a smart pointer class is not as straightforward as it seems—because of the special nature of this class. You see, we never want more than one SmartPtr object to serve as a smart pointer to a given X object. So we do not want copy construction and copy assignment operation to result in two different SmartPtr objects pointing to the same X object. We therefore need to embed in our class definition for SmartPtr the notion of owning a given X object and the notion of transferring this ownership if copy construction or copy assignment is called for. This notion can be enforced by defining a release() member function for the SmartPtr class:

      X* release() {         X* oldPtr = ptr;         ptr = 0;         return oldPtr;      } 

which simply releases a smart pointer's ownership of an X object by setting its ptr data member to 0. The function returns the old value of this pointer so that it can be handed over to some other smart pointer. In order to bring about this hand-over, we also need some sort of a reset member function for changing the X object owned by a smart pointer:

      void reset (X* newPtr) {         if ( ptr != newPtr) {             delete ptr;             ptr = newPtr;         }      } 

We can now write the following definitions for the copy constructor and the copy assignment operator:

    //copy constructor:    SmartPtr (SmartPtr& other) : ptr(other. release()) {}    //copy assignment operator:    SmartPtr operator=(SmartPtr& other) );        if ( this != &other)            reset (other .release())        return *this;    } 

Pulling all of these definitions together gives us a more complete smart pointer class for objects of type X:

 
//SmartPtrWithOwnership. cc class X {} class SmartPtr { X* ptr; public: explicit SmartPtr( X* p = 0) : ptr(p) {}; //(A) X&operator*() { return *ptr; } X* operator->() { return ptr; } SmartPtr( SmartPtr&other) : ptr(other. release()) {} SmartPtr operator=(SmartPtr&other) { if ( this != &other) reset(other. release()); return *this; } ~SmartPtr() { delete ptr; } X* release() { X* oldPtr = ptr; ptr = 0; return oldPtr; } void reset (X* newPtr) { if ( ptr != newPtr) { delete ptr; ptr = newPtr; } } }; // end of SmartPtr class int main() { X* xp = new X(); SmartPtr s1(xp); SmartPtr s2 = s1; // test copy const (s2 now owns X object) SmartPtr s3; // use no-arg constructor s3 = s2; // test copy assign (s3 now owns X object) return 0; }

where we have also included a no-arg constructor for SmartPtr by giving a default value of 0 to the pointer argument of the one-arg constructor in line (A). The keyword explicit that qualifies the constructor is to prevent the use of this constructor for implicit type conversion, as explained in Section 12.9

The SmartPtr class still has one operational deficiency associated with it—it is custom designed for holding objects of type X. Going back to the MyClass example in the ConstructorLeak.cc program at the beginning of this section, this would imply creating separate smart pointer classes for Giant and Big. Fortunately, the notion of a template class that we will discuss in some detail in Chapter 13 makes that unnecessary. A templatized version of the SmartPtr class would be along the following lines:

 
//SmartPtr. h template<class T> class SmartPtr { T* ptr; public: explicit SmartPtr( T* p = 0) : ptr(p) {} T& operator*() const { return *ptr; } T* operator->() const { return ptr; } SmartPtr( SmartPtr<T>& other) : ptr(other. release()) {} SmartPtr operator=(SmartPtr<T>& other) { if ( this != &other) reset (other .release()); return *this; } ~SmartPtr() { delete ptr; } T* release() { T* oldPtr = ptr; ptr = 0; return oldPtr; } void reset (T* newPtr) { if ( ptr != newPtr) { delete ptr; ptr = newPtr; } } };

Now we can reprogram the ConstructorLeak.cc program shown earlier in the manner shown below using the template class through the header SmartPtr.h. The smart-pointer based implementation shown below does not suffer from the resource leak that was present in the earlier implementation.

 
//ConstructorLeakPlugged . cc #include "SmartPtr. h" #include <iostream> using namespace std; class Err{}; class Giant { public: ~Giant() {cout << "Giant's destructor invoked" << endl;} }; class Big { public: Big() throw(Err) { throw Err();} ~Big(){cout << "Big's destructor invoked" << endl;} }; class MyClass { SmartPtr<Giant> giant; SmartPtr<Big> big; public: MyClass() : giant (0), big(0) { giant. reset (new Giant());{ big.reset(new Big()); } ~MyClass() {} // no destructor needed anymore }; int main(){ } try { Myclass myclass; } catch (Err) {} return 0; }

Fortunately for the programmer, the C++ Standard Library comes equipped comes with a smart pointer class, auto_ptr, in the header file <memory> that has all the smarts we talked about in this section. The reader is referred to p. 368 of Stroustrup [54] and pp. 291-294 of Meyers [51] for the definition of the auto_ptr class.

[4]The dereferencing operator-> is also known as the member access operator.

[5]See Lippman and Lajoie [50, p. 757] for a more thorough discussion concerning this point.




Programming With Objects[c] A Comparative Presentation of Object-Oriented Programming With C++ and Java
Programming with Objects: A Comparative Presentation of Object Oriented Programming with C++ and Java
ISBN: 0471268526
EAN: 2147483647
Year: 2005
Pages: 273
Authors: Avinash Kak

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