13.2.2. Defining Classes That Act Like Pointers

Image

For our HasPtr class to act like a pointer, we need the copy constructor and copy-assignment operator to copy the pointer member, not the string to which that pointer points. Our class will still need its own destructor to free the memory allocated by the constructor that takes a string13.1.4, p. 504). In this case, though, the destructor cannot unilaterally free its associated string. It can do so only when the last HasPtr pointing to that string goes away.

The easiest way to make a class act like a pointer is to use shared_ptrs to manage the resources in the class. Copying (or assigning) a shared_ptr copies (assigns) the pointer to which the shared_ptr points. The shared_ptr class itself keeps track of how many users are sharing the pointed-to object. When there are no more users, the shared_ptr class takes care of freeing the resource.

However, sometimes we want to manage a resource directly. In such cases, it can be useful to use a reference count12.1.1, p. 452). To show how reference counting works, we’ll redefine HasPtr to provide pointerlike behavior, but we will do our own reference counting.

Reference Counts

Reference counting works as follows:

• In addition to initializing the object, each constructor (other than the copy constructor) creates a counter. This counter will keep track of how many objects share state with the object we are creating. When we create an object, there is only one such object, so we initialize the counter to 1.

• The copy constructor does not allocate a new counter; instead, it copies the data members of its given object, including the counter. The copy constructor increments this shared counter, indicating that there is another user of that object’s state.

• The destructor decrements the counter, indicating that there is one less user of the shared state. If the count goes to zero, the destructor deletes that state.

• The copy-assignment operator increments the right-hand operand’s counter and decrements the counter of the left-hand operand. If the counter for the left-hand operand goes to zero, there are no more users. In this case, the copy-assignment operator must destroy the state of the left-hand operand.

The only wrinkle is deciding where to put the reference count. The counter cannot be a direct member of a HasPtr object. To see why, consider what happens in the following example:

HasPtr p1("Hiya!");
HasPtr p2(p1);  // p1 and p2 point to the same string
HasPtr p3(p1);  // p1, p2, and p3 all point to the same string

If the reference count is stored in each 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?

One way to solve this problem is to store the counter in dynamic memory. When we create an object, we’ll also allocate a new counter. When we copy or assign an object, we’ll copy the pointer to the counter. That way the copy and the original will point to the same counter.

Defining a Reference-Counted Class

Using a reference count, we can write the pointerlike version of HasPtr as follows:

class HasPtr {
public:
    // constructor allocates a new string and a new counter, which it sets to 1
    HasPtr(const std::string &s = std::string()):
      ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
    // copy constructor copies all three data members and increments the counter
    HasPtr(const HasPtr &p):
        ps(p.ps), i(p.i), use(p.use) { ++*use; }
    HasPtr& operator=(const HasPtr&);
    ~HasPtr();
private:
    std::string *ps;
    int    i;
    std::size_t *use;   // member to keep track of how many objects share *ps
};

Here, we’ve added a new data member named use that will keep track of how many objects share the same string. The constructor that takes a string allocates this counter and initializes it to 1, indicating that there is one user of this object’s string member.

Pointerlike Copy Members “Fiddle” the Reference Count

When we copy or assign a HasPtr object, we want the copy and the original to point to the same string. That is, when we copy a HasPtr, we’ll copy ps itself, not the string to which ps points. When we make a copy, we also increment the counter associated with that string.

The copy constructor (which we defined inside the class) copies all three members from its given HasPtr. This constructor also increments the use member, indicating that there is another user for the string to which ps and p.ps point.

The destructor cannot unconditionally delete ps—there might be other objects pointing to that memory. Instead, the destructor decrements the reference count, indicating that one less object shares the string. If the counter goes to zero, then the destructor frees the memory to which both ps and use point:

HasPtr::~HasPtr()
{
    if (--*use == 0) {    // if the reference count goes to 0
        delete ps;        // delete the string
        delete use;       // and the counter
    }
}

The copy-assignment operator, as usual, does the work common to the copy constructor and to the destructor. That is, the assignment operator must increment the counter of the right-hand operand (i.e., the work of the copy constructor) and decrement the counter of the left-hand operand, deleting the memory used if appropriate (i.e., the work of the destructor).

Also, as usual, the operator must handle self-assignment. We do so by incrementing the count in rhs before decrementing the count in the left-hand object. That way if both objects are the same, the counter will have been incremented before we check to see if ps (and use) should be deleted:

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    ++*rhs.use;  // increment the use count of the right-hand operand
    if (--*use == 0) {  // then decrement this object's counter
        delete ps;      // if no other users
        delete use;     // free this object's allocated members
    }
    ps = rhs.ps;        // copy data from rhs into this object
    i = rhs.i;
    use = rhs.use;
    return *this;       // return this object
}


Exercises Section 13.2.2

Exercise 13.27: Define your own reference-counted version of HasPtr.

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

(a)

class TreeNode {
 private:
     std::string value;
     int         count;
     TreeNode    *left;
     TreeNode    *right;
 };

(b)

class BinStrTree {
      private:
           TreeNode *root;
      };


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

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