Case Study: Smart Pointers

Chapter 9 mentions some problems with using dynamically allocated objects in C++. Eventually, someone must remember to dispose of them. If, however, they are thrown out before someone is finished with them, there is trouble. (People with small children should recognize the general problem.) One common solution is reference counting, which you saw working behind the scenes of the Array class in Chapter 9. It's clear when a particular object still has users, and premature disposal cannot happen.

However, to use reference-counting you have to do a certain amount of work. Whenever an object keeps a pointer to a reference-counted object, it must increment the reference count. It is equally important to call dispose() afterward. It would be nice if this could happen automatically. This can be done, and the extra cost is not too great. You need to define smart pointers, which are objects that behave like object pointers and can be very useful. The standard library provides such a pointer, called auto_ptr<T>, and here is a simplified implementation of it:


template <class T>
  class auto_ptr {
    T *m_ptr;
  public:
    auto_ptr(T *p) : m_ptr(p) { }
    ~auto_ptr() {  delete m_ptr; }

    T& operator* () const {  return *m_ptr; }
    T* operator->() const {  return m_ptr; }
    operator T * () const {  return m_ptr;  }
 };

void test_ptr() {
  auto_ptr<Person> pp (new Person());
  pp->set_name("Billy");    // operator->
  read(pp);                 // can match Person * (conversion)
  if (pp->illegal()) throw Person::Bad();
  dump(*pp);                // operator* —- Person&
}   // pp is destroyed.

In this example, the class auto_ptr is stripped down to its basics; it is a wrapper around an object pointer m_ptr. When pp (which is of type auto_ptr<Person>) is automatically destroyed at the end of test_ptr(), its destructor disposes of m_ptr. Otherwise, it behaves just like a Person * object because of the operator overloads. No matter how you leave the function, test_ptr(), pp is destroyed, and its pointer is deleted. (The full version of auto_ptr<> also manages who gets to delete the pointer, but that's not relevant here.)

The overloads are simple, but operator-> is one we haven't talked about yet. Normally, an object is not followed by ->, but if it is, the compiler looks for an overloaded operator->. If the compiler finds such an operator, it looks at the return type, and if the return type is T *, the compiler looks for any members of T after ->.

By using this technique you can fashion pointers with any behavior you choose. It is simple to track access to objects, for example. The idea is to design a smart pointer that makes using reference-counted objects easy, so all objects in the system must derive from RefCount. Here is a smart pointer ptr that manages an object pointer derived from RefCount. The method set() is the interesting one:


// ptr.h
template <class T>
 class ptr {
   T *m_p;
 public:
   void set(T *p) {
     if (m_p != NULL) m_p->dispose();
     m_p = p;
     if (m_p != NULL) m_p->incr_ref();
   }

   ptr(T *p = NULL)
    : m_p(NULL)
    {  set(p);    }

   ptr(const ptr& pp)
   : m_p(NULL)
   {  set(pp.m_p);  }

   ptr& operator= (const ptr& pp) {
     set(pp.m_p);
     return *this;
   }
   ptr& operator= (T *p) {
     set(p);
     return *this;
   }

   void unique()
   { set(new T(*m_p)); }

   bool is_empty() const {  return m_p == NULL; }

   T& operator*  () const {  return *m_p; }
   T* operator-> () const {  return m_p; }
   operator T *  () const {  return m_p; }

  };

The last three overloads in this class give you the smart pointer interface. The business of managing reference counting is managed by the set() method, which works as follows. If the ptr object already has a pointer, you call dispose(). (Remember that this does not necessarily delete the object; it does so only if it has no other users.) The new pointer's reference count is then incremented, and the smart pointer is ready for business.

unique() allows you to force a unique copy of the pointer. It depends on a sensible copy constructor being available, that is, that the constructor T(const T&) does generate a proper copy of the object. Here is some example code that shows how ptr is used. Note that you may freely pass ptr<Person> objects around; they behave just like Person*, but quietly dispose of Person pointers when they are no longer needed:

void test1(ptr<Person> p) {
  ptr<Person> p_new(new Person(p));
  read(p_new);
  dump(p_new);
};  // p_new's object is deleted
    // p's isn't (still owned)

ptr<Person> test2() {
  return ptr<Person> (new Person());
}

void test2() {
 ptr<Person> p1(new Person());
 test1(p1);
 ptr<Person> p2 = test2();
}  // p1 & p2 destroyed.

This example shows that ptr<Person> manages the life of the Person pointer. If this pointer is consistently used, there is no need for an explicit delete p. Deleting from a list of such objects would also cause disposal, as with value-oriented types such as std::string. But ptr<Person> still obeys pointer semantics; that is, assigning a ptr<Person> to another ptr<Person> merely shares a reference to the same Person pointer, without any potentially expensive copying taking place. This case study shows that it is possible to use pointers in a safe fashion in C++.

Do these smart pointers maintain the same relationship between their pointers? That is, does a ptr<Employee> match a ptr<Person>, given that Employee is a derived class of Person? This code demonstrates that it is still possible to assign ptr<Employee> to ptr<Person>:

;> ptr<Employee> pe (new Employee());
;> ptr<Person> pp;
;> pp = pe;             // cool!
;> pe = pp;             // bad!
					

The correct assignment in this example takes place in two steps. Obviously, no assignment operators can do this directly, because ptr<Employee> is not related to ptr<Person>. When no standard conversion is possible, C++ will look for user-defined conversion operators. ptr<Employee> has a user-defined conversion operator operator Employee*, and so pe will be converted to an Employee pointer.

The next step happens because an Employee pointer can match a Person pointer, so the compiler can use Person's assignment operator operator= (Person *). Maintaining the relationship between the various pointer types is the crucial part of using ptr.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.247.53