Section 13.5. Managing Pointer Members


13.5. Managing Pointer Members

This book generally advocates the use of the standard library. One reason we do so is that using the standard library greatly reduces the need for pointers in modern C++ programs. However, many applications still require the use of pointers, particularly in the implementation of classes. Classes that contain pointers require careful attention to copy control. The reason they must do so is that copying a pointer copies only the address in the pointer. Copying a pointer does not copy the object to which the pointer points.

When designing a class with a pointer member, the first decision a class author must make is what behavior that pointer should provide. When we copy one pointer to another, the two pointers point to the same object. When two pointers point to the same object, it is possible to use either pointer to change the underlying object. Similarly, it is possible for one pointer to delete the object even though the user of the other pointer still thinks the underlying object exists.

By default, a pointer member has the same behavior as a pointer object. However, through different copy-control strategies we can implement different behavior for pointer members. Most C++ classes take one of three approaches to managing pointer members:

  1. The pointer member can be given normal pointerlike behavior. Such classes will have all the pitfalls of pointers but will require no special copy control.

  2. The class can implement so-called "smart pointer" behavior. The object to which the pointer points is shared, but the class prevents dangling pointers.

  3. The class can be given valuelike behavior. The object to which the pointer points will be unique to and managed separately by each class object.

In this section we look at three classes that implement each of these different approaches to managing their pointer members.

A Simple Class with a Pointer Member

To illustrate the issues involved, we'll implement a simple class that contains an int and a pointer:

      // class that has a pointer member that behaves like a plain pointer      class HasPtr {      public:          // copy of the values we're given          HasPtr(int *p, int i): ptr(p), val(i) { }          // const members to return the value of the indicated data member          int *get_ptr() const { return ptr; }          int get_int() const { return val; }          // non const members to change the indicated data member          void set_ptr(int *p) { ptr = p; }          void set_int(int i) { val = i; }          // return or change the value pointed to, so ok for const objects          int get_ptr_val() const { return *ptr; }          void set_ptr_val(int val) const { *ptr = val; }      private:          int *ptr;          int val;      }; 

The HasPtr constructor takes two parameters, which it copies into HasPtr's data members. The class provides simple accessor functions: The const functions get_int and get_ptr return the value of the int and pointer members, respectively; the set_int and set_ptr members let us change these members, giving a new value to the int or making the pointer point to a different object. We also define the get_ptr_val and set_ptr_val members. These members get and set the underlying value to which the pointer points.

Default Copy/Assignment and Pointer Members

Because the class does not define a copy constructor, copying one HasPtr object to another copies both members:

      int obj = 0;      HasPtr ptr1(&obj, 42); // int* member points to obj, val is 42      HasPtr ptr2(ptr1);     // int* member points to obj, val is 42 

After the copy, the pointers in ptr1 and ptr1 both address the same object and the int values in each object are the same. However, the behavior of these two members appears quite different, because the value of a pointer is distinct from the value of the object to which it points. After the copy, the int values are distinct and independent, whereas the pointers are intertwined.

Classes that have pointer members and use default synthesized copy control have all the pitfalls of ordinary pointers. In particular, the class itself has no way to avoid dangling pointers.



Pointers Share the Same Object

When we copy an arithmetic value, the copy is independent from the original. We can change one copy without changing the other:

      ptr1.set_int(0); // changes val member only in ptr1      ptr2.get_int();  // returns 42      ptr1.get_int();  // returns 0 

When we copy a pointer, the address values are distinct, but the pointers point to the same underlying object. If we call set_ptr_val on either object, the underlying object is changed for both:

      ptr1.set_ptr_val(42); // sets object to which both ptr1 and ptr2 point      ptr2.get_ptr_val();   // returns 42 

When two pointers point to the same object, either one can change the value of the shared object.

Dangling Pointers Are Possible

Because our class copies the pointers directly, it presents our users with a potential problem: HasPtr stores the pointer it was given. It is up to the user to guarantee that the object to which that pointer points stays around as long as the HasPtr object does:

      int *ip = new int(42); // dynamically allocated int initialized to 42      HasPtr ptr(ip, 10);    // Has Ptr points to same object as ip does      delete ip;             // object pointed to by ip is freed      ptr.set_ptr_val(0); // disaster: The object to which Has Ptr points was freed! 

The problem here is that ip and the pointer inside ptr both point to the same object. When that object is deleted, the pointer inside HasPtr no longer points to a valid object. However, there is no way to know that the object is gone.

Exercises Section 13.5

Exercise 13.20:

Given the original version of the HasPtr class that relies on the default definitions for copy-control, describe what happens in the following code:

      int i = 42;      HasPtr p1(&i, 42);      HasPtr p2 = p1;      cout << p2.get_ptr_val() << endl;      p1.set_ptr_val(0);      cout << p2.get_ptr_val() << endl; 

Exercise 13.21:

What would happen if we gave our HasPtr class a destructor that deleted its pointer member?


13.5.1. Defining Smart Pointer Classes

In the previous section we defined a simple class that held a pointer and an int. The pointer member behaved in all ways like any other pointer. Any changes made to the object to which the pointer pointed were made to a single, shared object. If the user deleted that object, then our class had a dangling pointer. Its pointer member pointed at an object that no longer existed.

An alternative to having a pointer member behave exactly like a pointer is to define what is sometimes referred to as a smart pointer class. A smart pointer behaves like an ordinary pointer except that it adds functionality. In this case, we'll give our smart pointer the responsibility for deleting the shared object. Users will dynamically allocate an object and pass the address of that object to our new HasPtr class. The user may still access the object through a plain pointer but must not delete the pointer. The HasPtr class will ensure that the object is deleted when the last HasPtr that points to it is destroyed.

In other ways, our HasPtr will behave like a plain pointer. In particular, when we copy a HasPtr object, the copy and the original will point to the same underlying object. If we change that object through one copy, the value will be changed when accessed through the other.

Our new HasPtr class will need a destructor to delete the pointer. However, the destructor cannot delete the pointer unconditionally. If two HasPtr objects point to the same underlying object, we don't want to delete the object until both objects are destroyed. To write the destructor, we need to know whether this HasPtr is the last one pointing to a given object.

Introducing Use Counts

A common technique used in defining smart pointers is to use a use count. The pointerlike class associates a counter with the object to which the class points. The use count keeps track of how many objects of the class share the same pointer. When the use count goes to zero, then the object is deleted. A use count is sometimes also referred to as a reference count.

Each time a new object of the class is created, the pointer is initialized and the use count is set to 1. When an object is created as a copy of another, the copy constructor copies the pointer and increments the associated use count. When an object is assigned to, the assignment operator decrements the use count of the object to which the left-hand operand points (and deletes that object if the use count goes to zero) and increments the use count of the object pointed to by the right-hand operand. Finally, when the destructor is called, it decrements the use count and deletes the underlying object if the count goes to zero.

The only wrinkle is deciding where to put the use count. The counter cannot go directly into our HasPtr object. To see why, consider what happens in the following case:

      int obj;      HasPtr p1(&obj, 42);      HasPtr p2(p1);  // p1 and p2 both point to same int object      HasPtr p3(p1);  // p1, p2, and p3 all point to same int object 

If the use count is stored in a HasPtr object, how can we update it correctly when p3 is created? We could increment the count in p1 and copy that count into p3, but how would we update the counter in p2?

The Use-Count Class

There are two classic strategies for implementing a use count, one of which we will use here; the other approach is described in Section 15.8.1 (p. 599). In the approach we use here, we'll define a separate concrete class to encapsulate the use count and the associated pointer:

      // private class for use by HasPtr only      class U_Ptr {          friend class HasPtr;          int *ip;          size_t use;          U_Ptr(int *p): ip(p), use(1) { }          ~U_Ptr() { delete ip; }      }; 

All the members of this class are private. We don't intend ordinary users to use the U_Ptr class, so we do not give it any public members. The HasPtr class is made a friend so that its members will have access to the members of U_Ptr.

The class is pretty simple, although the concept of how it works can be slippery. The U_Ptr class holds the pointer and the use count. Each HasPtr will point to a U_Ptr. The use count will keep track of how many HasPtr objects point to each U_Ptr object. The only functions U_Ptr defines are its constructor and destructor. The constructor copies the pointer, which the destructor deletes. The constructor also sets the use count to 1, indicating that a HasPtr object points to this U_Ptr.

Assuming we just created a HasPtr object from a pointer that pointed to an int value of 42, we might picture the objects as follows:

If we copy this object, then the objects will be as shown on the next page.

Using the Use-Counted Class

Our new HasPtr class holds a pointer to a U_Ptr, which in turn points to the actual underlying int object. Each member must be changed to reflect the fact that the class points to a U_Ptr rather than an int*.

We'll look first at the constructors and copy-control members:

      /* smart pointer class: takes ownership of the dynamically allocated       *          object to which it is bound       * User code must dynamically allocate an object to initialize a HasPtr       * and must not delete that object; the HasPtr class will delete it       */      class HasPtr {      public:          // HasPtr owns the pointer; pmust have been dynamically allocated          HasPtr(int *p, int i): ptr(new U_Ptr(p)), val(i) { }          // copy members and increment the use count          HasPtr(const HasPtr &orig):             ptr(orig.ptr), val(orig.val) { ++ptr->use; }          HasPtr& operator=(const HasPtr&);          // if use count goes to zero, delete the U_Ptr object          ~HasPtr() { if (--ptr->use == 0) delete ptr; }      private:          U_Ptr *ptr;        // points to use-counted U_Ptr class          int val;      }; 

The HasPtr constructor that takes a pointer and an int uses its pointer parameter to create a new U_Ptr object. After the HasPtr constructor completes, the HasPtr object points to a newly allocated U_Ptr object. That U_Ptr object stores the pointer we were given. The use count in that new U_Ptr is 1, indicating that only one HasPtr object points to it.

The copy constructor copies the members from its parameter and increments the use count. After the constructor completes, the newly created object points to the same U_Ptr object as the original and the use count of that U_Ptr object is incremented by one.

The destructor checks the use count in the underlying U_Ptr object. If the use count goes to 0, then this is the last HasPtr object that points to this U_Ptr. In this case, the HasPtr destructor deletes its U_Ptr pointer. Deleting that pointer has the effect of calling the U_Ptr destructor, which in turn deletes the underlying int object.

Assignment and Use Counts

The assignment operator is a bit more complicated than the copy constructor:

      HasPtr& HasPtr::operator=(const HasPtr &rhs)      {          ++rhs.ptr->use;     // increment use count on rhs first          if (--ptr->use == 0)               delete ptr;    // if use count goes to 0 on this object, delete it          ptr = rhs.ptr;      // copy the U_Ptr object          val = rhs.val;      // copy the int member          return *this;      } 

Here we start by incrementing the use count in the right-hand operand. Then we decrement and check the use count on this object. As with the destructor, if this is the last object pointing to the U_Ptr, we delete the object, which in turn destroys the underlying int. Having decremented (and possibly destroyed) the existing value in the left-hand operand, we then copy the pointer from rhs into this object. As usual, assignment returns a reference to this object.

This assignment operator guards against self-assignment by incrementing the use count of rhs before decrementing the use count of the left-hand operand.



If the left and right operands are the same, the effect of this assignment operator will be to increment and then immediately decrement the use count in the underlying U_Ptr object.

Changing Other Members

The other members that access the int* now need to change to get to the int indirectly through the U_Ptr pointer:

      class HasPtr {      public:          // copy control and constructors as before          // accessors must change to fetch value from U_Ptr object          int *get_ptr() const { return ptr->ip; }          int get_int() const { return val; }          // change the appropriate data member          void set_ptr(int *p) { ptr->ip = p; }          void set_int(int i) { val = i; }          // return or change the value pointed to, so ok for const objects          // Note: *ptr->ip is equivalent to *(ptr->ip)          int get_ptr_val() const { return *ptr->ip; }          void set_ptr_val(int i) { *ptr->ip = i; }      private:          U_Ptr *ptr;        // points to use-counted U_Ptr class          int val;      }; 

The functions that get and set the int member are unchanged. Those that operate on the pointer have to dereference the U_Ptr to get to the underlying int*.

When we copy HasPtr objects, the int member behaves the same as in our first class. Its value is copied; the members are independent. The pointer members in the copy and the original still point to the same underlying object. A change made to that object will affect the value as seen by either HasPtr object. However, users of HasPtr do not need to worry about dangling pointers. As long as they let the HasPtr class take care of freeing the object, the class will ensure that the object stays around as long as there are HasPtr objects that point to it.

Advice: Managing Pointer Members

Objects with pointer members often need to define the copy-control members. If we rely on the synthesized versions, then the class puts a burden on its users. Users must ensure that the object to which the member points stays around for at least as long as the object that points to it does.

To manage a class with pointer members, we must define all three copy-control members: the copy constructor, assignment operator, and the destructor. These members can define either pointerlike or valuelike behavior for the pointer member.

Valuelike classes give each object its own copy of the underlying values pointed to by pointer members. The copy constructor allocates a new element and copies the value from the object it is copying. The assignment operator destroys the existing object it holds and copies the value from its right-hand operand into its left-hand operand. The destructor destroys the object.

As an alternative to defining either valuelike behavior or pointerlike behavior some classes are so-called "smart pointers." These classes share the same underlying value between objects, thus providing pointerlike behavior. But they use copy-control techniques to avoid some of the pitfalls of regular pointers. To implement smart pointer behavior, a class needs to ensure that the underlying object stays around until the last copy goes away. Use counting (Section 13.5.1, p. 495), is a common technique for managing smart pointer classes. Each copy of the same underlying value is given a use count. The copy constructor copies the pointer from the old object into the new one and increments the use count. The assignment operator decrements the use count of the left-hand operand and increments the count of the right-hand operand. If the use count of the left-hand operand goes to zero, the assignment operator must delete the object to which it points. Finally, the assignment operator copies the pointer from the right-hand operand into its left-hand operand. The destructor decrements the use count and deletes the underlying object if the count goes to zero.

These approaches to managing pointers occur so frequently that programmers who use classes with pointer members must be thoroughly familiar with these programming techniques.




13.5.2. Defining Valuelike Classes

A completely different approach to the problem of managing pointer members is to give them value semantics. Simply put, classes with value semantics define objects that behave like the arithmetic types: When we copy a valuelike object, we get a new, distinct copy. Changes made to the copy are not reflected in the original, and vice versa. The string class is an example of a valuelike class.

Exercises Section 13.5.1

Exercise 13.22:

What is a use count?

Exercise 13.23:

What is a smart pointer? How does a smart pointer class differ from one that implements plain pointer behavior?

Exercise 13.24:

Implement your own version of the use-counted HasPtr class.


To make our pointer member behave like a value, we must copy the object to which the pointer points whenever we copy the HasPtr object:

      /*       * Valuelike behavior even though HasPtr has a pointer member:       * Each time we copy a HasPtr object, we make a new copy of the       * underlying int object to which ptr points.       */      class HasPtr {      public:          // no point to passing a pointer if we're going to copy it anyway          // store pointer to a copy of the object we're given          HasPtr(const int &p, int i): ptr(new int(p)), val(i) {}          // copy members and increment the use count          HasPtr(const HasPtr &orig):             ptr(new int (*orig.ptr)), val(orig.val) { }          HasPtr& operator=(const HasPtr&);          ~HasPtr() { delete ptr; }          // accessors must change to fetch value from Ptr object          int get_ptr_val() const { return *ptr; }          int get_int() const { return val; }          // change the appropriate data member          void set_ptr(int *p) { ptr = p; }          void set_int(int i) { val = i; }          // return or change the value pointed to, so ok for const objects          int *get_ptr() const { return ptr; }          void set_ptr_val(int p) const { *ptr = p; }      private:          int *ptr;        // points to an int          int val;      }; 

The copy constructor no longer copies the pointer. It now allocates a new int object and initializes that object to hold the same value as the object of which it is a copy. Each object always holds its own, distinct copy of its int value. Because each object holds its own copy, the destructor unconditionally deletes the pointer.

The assignment operator doesn't need to allocate a new object. It just has to remember to assign a new value to the object to which its int pointer points rather than assigning to the pointer itself:

      HasPtr& HasPtr::operator=(const HasPtr &rhs)      {          // Note: Every HasPtr is guaranteed to point at an actual int;          //    We know that ptr cannot be a zero pointer          *ptr = *rhs.ptr;       // copy the value pointed to          val = rhs.val;         // copy the int          return *this;      } 

In other words, we change the value pointed to but not the pointer.

As always, the assignment operator must be correct even if we're assigning an object to itself. In this case, the operations are inherently safe even if the left- and right-hand objects are the same. Thus, there is no need to explicitly check for self-assignment.



Exercises Section 13.5.2

Exercise 13.25:

What is a valuelike class?

Exercise 13.26:

Implement your own version of a valuelike HasPtr class.

Exercise 13.27:

The valuelike HasPtr class defines each of the copy-control members. Describe what would happen if the class defined

  1. The copy constructor and destructor but no assignment operator.

  2. The copy constructor and assignment operator but no destructor.

  3. The destructor but neither the copy constructor nor assignment operator.

Exercise 13.28:

Given the following classes, implement a default constructor and the necessary copy-control members.

      (a) class TreeNode {        (b) class BinStrTree {          public:                         public:              // ...                         //...          private:                        private:              std::string value;               TreeNode *root;              int         count;          };              TreeNode    *left;              TreeNode    *right;          }; 




C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

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