Chapter 13. Copy Control

CONTENTS

Section 13.1 The Copy Constructor 476

Section 13.2 The Assignment Operator 482

Section 13.3 The Destructor 484

Section 13.4 A Message-Handling Example 486

Section 13.5 Managing Pointer Members 492

Chapter Summary 502

Defined Terms 502

Each type, whether a built-in or class type, defines the meaning of a (possibly empty) set of operations on objects of that type. We can add two int values, run size on a vector, and so on. These operations define what can be done with objects of the given type.

Each type also defines what happens when objects of the type are created. Initialization of objects of class type is defined by constructors. Types also control what happens when objects of the type are copied, assigned, or destroyed. Classes control these actions through special member functions: the copy constructor, the assignment operator, and the destructor. This chapter covers these operations.

When we define a new type, we specify—explicitly or implicitly—what happens when objects of that type are copied, assigned, and destroyed. We do so by defining special members: the copy constructor, the assignment operator, and the destructor. If we do not explicitly define the copy constructor or the assignment operator, the compiler will (usually) define them for us.

The copy constructor is a special constructor that has a single parameter that is a (usually const) reference to the class type. The copy constructor is used explicitly when we define a new object and initialize it from an object of the same type. It is used implicitly when we pass or return objects of that type to or from functions.

The destructor is complementary to the constructors: It is applied automatically when an object goes out of scope or when a dynamically allocated object is deleted. The destructor is used to free resources acquired when the object was constructed or during the lifetime of the object. Regardless of whether a class defines its own destructor, the compiler automatically executes the destructors for the nonstatic data members of the class.

We’ll learn more about operator overloading in the next chapter, but in this chapter we cover the assignment operator. Like constructors, the assignment operator may be overloaded by specifying different types for the right-hand operand. The version whose right-hand operand is of the class type is special: If we do not write one, the compiler will synthesize one for us.

Collectively, the copy constructor, assignment operator, and destructor are referred to as copy control. The compiler automatically implements these operations, but the class may define its own versions.

image

Copy control is an essential part of defining any C++ class. Programmers new to C++ are often confused by having to define what happens when objects are copied, assigned, or destroyed. This confusion is compounded because if we do not explicitly define these operations, the compiler defines them for us—although they might not behave as we intend.

Often the compiler-synthesized copy-control functions are fine—they do exactly the work that needs to be done. But for some classes, relying on the default definitions leads to disaster. Frequently, the most difficult part of implementing the copy-control operations is recognizing when we need to override the default versions. One especially common case that requires the class to define its own the copy-control members is if the class has a pointer member.

13.1 The Copy Constructor

The constructor that takes a single parameter that is a (usually const) reference to an object of the class type itself is called the copy constructor. Like the default constructor, the copy constructor can be implicitly invoked by the compiler. The copy constructor is used to

• Explicitly or implicitly initialize one object from another of the same type

• Copy an object to pass it as an argument to a function

Copy an object to return it from a function

• Initialize the elements in a sequential container

• Initialize elements in an array from a list of element initializers

Forms of Object Definition

Recall that C++ supports two forms of initialization (Section 2.3.3, p. 48): direct and copy. Copy-initialization uses the = symbol, and direct-initialization places the initializer in parentheses.

The copy and direct forms of initialization, when applied to objects of class type, are subtly different. Direct-initialization directly invokes the constructor matched by the arguments. Copy-initialization always involves the copy constructor. Copy-initialization first uses the indicated constructor to create a temporary object (Section 7.3.2, p. 247). It then uses the copy constructor to copy that temporary into the one we are creating:

image

For objects of class type, copy-initialization can be used only when specifying a single argument or when we explicitly build a temporary object to copy.

When dots is created, the string constructor that takes a count and a character is called and directly initializes the members in dots. To create null_book, the compiler first creates a temporary by invoking the string constructor that takes a C-style character string parameter. The compiler then uses the string copy constructor to initialize null_book as a copy of that temporary.

The initialization of empty_copy and empty_direct both call the string default constructor. In the first case, the default constructor creates a temporary object, which is then used by the copy constructor to initialize empty_copy. In the second case, the default constructor is run directly on empty_direct.

The copy form of initialization is primarily supported for compatibility with C usage. When it can do so, the compiler is permitted (but not obligated) to skip the copy constructor and create the object directly.

Usually the difference between direct- or copy-initialization is at most a matter of low-level optimization. However, for types that do not support copying, or when using a constructor that is nonexplicit (Section 12.4.4, p. 462) the distinction can be essential:

image

The initialization of file1 is fine. The ifstream class defines a constructor that can be called with a C-style string. That constructor is used to initialize file1.

The seemingly equivalent initialization of file2 uses copy-initialization. That definition is not okay. We cannot copy objects of the IO types (Section 8.1, p. 287), so we cannot use copy-initialization on objects of these types.

Whether the initialization of item is okay depends on which version of our Sales_item class we are using. Some versions define the constructor that takes a string as explicit. If the constructor is explicit, then the initialization fails. If the constructor is not explicit, then the initialization is fine.

Parameters and Return Values

As we know, when a parameter is a nonreference type (Section 7.2.1, p. 230), the argument is copied. Similarly, a nonreference return value (Section 7.3.2, p. 247) is returned by copying the value in the return statement.

When the parameter or return type is a class type, the copy is done by the copy constructor. For example, consider our make_plural function from page 248:

     // copy constructor used to copy the return value;
     // parameters are references, so they aren't copied
     string make_plural(size_t, const string&, const string&);

This function implicitly uses the string copy constructor to return the plural version of a given word. The parameters are const references; they are not copied.

Initializing Container Elements

The copy constructor is used to initialize the elements in a sequential container. For example, we can initialize a container using a single parameter that represents a size (Section 3.3.1, p. 92). This form of construction uses both the default constructor and the copy constructor for the element container:

     // default string constructor and five string copy constructors invoked
     vector<string> svec(5);

The compiler initializes svec by first using the default string constructor to create a temporary value. The copy constructor is then used to copy the temporary into each element of svec.

image

As a general rule (Section 9.1.1, p. 307), unless you intend to use the default initial value of the container elements, it is more efficient to allocate an empty container and add elements as the values for those elements become known.

Constructors and Array Elements

If we provide no element initializers for an array of class type, then the default constructor is used to initialize each element. However, if we provide explicit element initializers using the normal brace-enclosed array initialization list (Section 4.1.1, p. 111), then each element is initialized using copy-initialization. An element of the appropriate type is created from the specified value, and then the copy constructor is used to copy that value to the corresponding element:

image

A value that can be used to invoke a single-argument constructor for the element type can be specified directly, as in the initializers for the first three elements. If we wish to specify no arguments or multiple arguments, we need to use the full constructor syntax, as we do in the initializer for the last element.

Exercises Section 13.1

Exercise 13.1: What is a copy constructor? When is it used?

Exercise 13.2: The second initialization below fails to compile. What can we infer about the definition of vector?

image

Exercise 13.3: Assuming Point is a class type with a public copy constructor, identify each use of the copy constructor in this program fragment:

image

13.1.1 The Synthesized Copy Constructor

If we do not otherwise define the copy constructor, the compiler synthesizes one for us. Unlike the synthesized default constructor (Section 12.4.3, p. 458), a copy constructor is synthesized even if we define other constructors. The behavior of the synthesized copy constructor is to memberwise initialize the new object as a copy of the original object.

By memberwise, we mean that taking each nonstatic member in turn, the compiler copies the member from the existing object into the one being created. With one exception, the type of each member determines what it means to copy it. The synthesized copy constructor directly copies the value of members of built-in type. Members of class type are copied by using the copy constructor for that class. The one exception concerns array members. Even though we ordinarily cannot copy an array, if a class has a member that is an array, then the synthesized copy constructor will copy the array. It does so by copying each element.

The simplest conceptual model of memberwise initialization is to think of the synthesized copy constructor as one in which each data member is initialized in the constructor initializer list. For example, given our Sales_item class, which has three data members

image

the synthesized Sales_item copy constructor would look something like:

image

13.1.2 Defining Our Own Copy Constructor

The copy constructor is the constructor that takes a single parameter that is a (usually const) reference to the class type:

image

Usually the parameter is a const reference, although we can also define the copy constructor to take a nonconst reference. Because the constructor is used (implicitly) to pass and return objects to and from functions, it usually should not be made explicit (Section 12.4.4, p. 462). The copy constructor should copy the members from its argument into the object that is being constructed.

For many classes, the synthesized copy constructor does exactly the work that is needed. Classes that contain only members that are of class type or members that are of built-in (but not pointer type) often can be copied without explicitly defining the copy constructor.

However, some classes must take control of what happens when objects are copied. Such classes often have a data member that is a pointer or that represents another resource that is allocated in the constructor. Other classes have bookkeeping that must be done whenever a new object is created. In both these cases, the copy constructor must be defined.

Often the hardest part about defining a copy constructor is recognizing that a copy constructor is needed. Defining the constructor is usually pretty easy once the need for the constructor is recognized. The copy constructor itself is defined like any other constructor: It has the same name as the name of the class, it has no return value, it may (should) use a constructor initializer to initialize the members of the newly created object, and it may do any other necessary work inside a function body.

We’ll look at examples of classes that require class-defined copy constructors in later sections. Section 13.4 (p. 486) looks at a pair of classes that require an explicit copy constructor to handle bookkeeping associated with a simple message-handling application; classes with members that are pointers are covered in Section 13.5 (p. 492).

Exercises Section 13.1.2

Exercise 13.4: Given the following sketch of a class, write a copy constructor that copies all the elements. Copy the object to which pstring points, not the pointer.

image

Exercise 13.5: Which class definition is likely to need a copy constructor?

(a) A Point3w class containing four float members

(b) A Matrix class in which the actual matrix is allocated dynamically within the constructor and is deleted within its destructor

(c) A Payroll class in which each object is provided with a unique ID

(d) A Word class containing a string and a vector of line and column location pairs

Exercise 13.6: The parameter of the copy constructor does not strictly need to be const, but it does need to be a reference. Explain the rationale for this restriction. For example, explain why the following definition could not work.

     Sales_item::Sales_item(const Sales_item rhs);

13.1.3 Preventing Copies

Some classes need to prevent copies from being made at all. For example, the iostream classes do not permit copying (Section 8.1, p. 287). It might seem that if we want to forbid copies, we could omit the copy constructor. However, if we don’t define a copy constructor, the compiler will synthesize one.

image

To prevent copies, a class must explicitly declare its copy constructor as private.

If the copy constructor is private, then user code will not be allowed to copy objects of the class type. The compiler will reject any attempt to make a copy.

However, the friends and members of the class could still make copies. If we want to prevent copies even within the friends and members, we can do so by declaring a (private) copy constructor but not defining it.

It is legal to declare but not define a member function. However, any attempt to use an undefined member results in a link-time failure. By declaring (but not defining) a private copy constructor, we can forestall any attempt to copy an object of the class type: Attempts to make copies in user code will be flagged as an error at compile time, and attempts to make copies in member functions or friends will result in an error at link time.

Most Classes Should Define Copy and Default Constructors

Classes that do not define the default constructor and/or the copy constructor impose serious limits on users of the class. Objects of classes that do not allow copies may be passed to (or returned from) a function only as a reference. They also may not be used as elements in a container.

image

It is usually best to define—either implicitly or explicitly—the default and copy constructors. The default constructor is synthesized only if there are no other constructors. If the copy constructor is defined, then the default constructor must be defined as well.

13.2 The Assignment Operator

Just as classes control how objects are initialized, they also define what happens when objects of their type are assigned:

     Sales_item trans, accum;
     trans = accum;

As with the copy constructor, the compiler synthesizes an assignment operator if the class does not define its own.

Introducing Overloaded Assignment

Before we look at the synthesized assignment operator, we need to know a bit about overloaded operators, which we cover in detail in Chapter 14.

Overloaded operators are functions that have the name operator followed by the symbol for the operator being defined. Hence, we define assignment by defining a function named operator=. Like any other function, an operator function has a return type and a parameter list. The parameter list must have the same number of parameters (including the implicit this parameter if the operator is a member) as the operator has operands. Assignment is binary, so the operator function has two parameters: The first parameter corresponds to the left-hand operand, and the second to the right-hand operand.

Most operators may be defined as member or nonmember functions. When an operator is a member function, its first operand is implicitly bound to the this pointer. Some operators, assignment among them, must be members of the class for which the operator is defined. Because assignment must be a member of its class, this is bound to a pointer to the left-hand operand. The assignment operator, therefore, takes a single parameter that is an object of the same class type. Usually, the right-hand operand is passed as a const reference.

The return type from the assignment operator should be the same as the return from assignment for the built-in types (Section 5.4.1, p. 160). Assignment to a built-in type returns a reference to its left-hand operand. Therefore, the assignment operator also returns a reference to the same type as its class.

For example, the assignment operator for Sales_item might be declared as

image

The Synthesized Assignment Operator

The synthesized assignment operator operates similarly to the synthesized copy constructor. It performs memberwise assignment: Each member of the right-hand object is assigned to the corresponding member of the left-hand object. Except for arrays, each member is assigned in the usual way for its type. For arrays, each array element is assigned.

As an example, the synthesized Sales_item assignment operator would look something like:

image

The synthesized assignment operator assigns each member in turn, using the built-in or class-defined assignment operator as appropriate to the type of the member. The operator returns *this, which is a reference to the left-hand object.

Copy and Assign Usually Go Together

Classes that can use the synthesized copy constructor usually can use the synthesized assignment operator as well. Our Sales_item class has no need to define either the copy constructor or the assignment operator: The synthesized versions of these operators work fine.

However, a class may define its own assignment operator. In general, if a class needs a copy constructor, it will also need an assignment operator.

image

In fact, these operations should be thought of as a unit. If we require one, we almost surely require the other.

We’ll see examples of classes that need to define their own assignment operators in Section 13.4 (p. 486) and Section 13.5 (p. 492).

Exercises Section 13.2

Exercise 13.7: When does a class need to define an assignment operator?

Exercise 13.8: For each type listed in the first exercise in Section 13.1.2 (p. 481) indicate whether the class would need an assignment operator.

Exercise 13.9: The first exercise in Section 13.1.2 (p. 481) included a skeletal definition for class NoName. Determine whether that class needs an assignment operator. If so, implement it.

Exercise 13.10: Define an Employee class that contains the employee’s name and a unique employee identifier. Give the class a default constructor and a constructor that takes a string representing the employee’s name. If the class needs a copy constructor or assignment operator, implement those functions as well.

13.3 The Destructor

One purpose of a constructor is to provide for the automatic acquisition of a resource. For example, a constructor might allocate a buffer or open a file. Having allocated the resource in the constructor, we need a corresponding operation that automatically deallocates or otherwise releases the resource. The destructor is a special member function that can be used to do whatever resource deallocation is needed. It serves as the complement to the constructors of the class.

When a Destructor Is Called

The destructor is called automatically whenever an object of its class is destroyed:

image

Variables such as item are destroyed automatically when they go out of scope. Hence, the destructor on item is run when the close curly is encountered.

An object that is dynamically allocated is destroyed only when a pointer pointing to the object is delete d. If we do not delete a pointer to a dynamically allocated object, then the destructor is never run on that object. The object will persist forever, leading to a memory leak. Moreover, any resources used inside the object will also not be released.

image

The destructor is not run when a reference or a pointer to an object goes out of scope. The destructor is run only when a pointer to a dynamically allocated object is deleted or when an actual object (not a reference to the object) goes out of scope.

Destructors are also run on the elements of class type in a container—whether a library container or built-in array—when the container is destroyed:

image

The elements in the container are always destroyed in reverse order: The element indexed by size() - 1 is destroyed first, followed by the one indexed by size() - 2 and so on until element [0], which is destroyed last.

When to Write an Explicit Destructor

Many classes do not require an explicit destructor. In particular, a class that has a constructor does not necessarily need to define its own destructor. Destructors are needed only if there is work for them to do. Ordinarily they are used to relinquish resources acquired in the constructor or during the lifetime of the object.

image

A useful rule of thumb is that if a class needs a destructor, it will also need the assignment operator and a copy constructor. This rule is often referred to as the Rule of Three, indicating that if you need a destructor, then you need all three copy-control members.

A destructor is not limited only to relinquishing resources. A destructor, in general, can perform any operation that the class designer wishes to have executed subsequent to the last use of an object of that class.

The Synthesized Destructor

Unlike the copy constructor or assignment operator, the compiler always synthesizes a destructor for us. The synthesized destructor destroys each nonstatic member in the reverse order from that in which the object was created. In consequence, it destroys the members in reverse order from which they are declared in the class. For each member that is of class type, the synthesized destructor invokes that member’s destructor to destroy the object.

image

Destroying a member of built-in or compound type has no effect. In particular, the synthesized destructor does not delete the object pointed to by a pointer member.

How to Write a Destructor

Our Sales_item class is an example of a class that allocates no resources and so does not need its own destructor. Classes that do allocate resources usually need to define a destructor to free those resources. The destructor is a member function with the name of the class prefixed by a tilde (~). It has no return value and takes no parameters. Because it cannot specify any parameters, it cannot be overloaded. Although we can define multiple class constructors, we can provide only a single destructor to be applied to all objects of our class.

An important difference between the destructor and the copy constructor or assignment operator is that even if we write our own destructor, the synthesized destructor is still run. For example, we might write the following empty destructor for class Sales_item:

image

When objects of type Sales_item are destroyed, this destructor, which does nothing, would be run. After it completes, the synthesized destructor would also be run to destroy the members of the class. The synthesized destructor destroys the string member by calling the string destructor, which frees the memory used to hold the isbn. The units_sold and revenue members are of built-in type, so the synthesized destructor does nothing to destroy them.

13.4 A Message-Handling Example

As an example of a class that needs to control copies in order to do some bookkeeping, we’ll sketch out two classes that might be used in a mail-handling application. These classes, Message and Folder, represent, respectively, email (or other) messages and directories in which a message might appear. A given Message might appear in more than one Folder. We’ll have save and remove operations on Message that save or remove that message in the specified Folder.

Rather than putting a copy of each Message into each Folder, we’ll have each Message hold a set of pointers to the Folders in which this Message appears. Each Folder will also store pointers to the Messages it contains. Figure 13.1 (p. 488) illustrates the data structure we’ll implement.

Figure 13.1. Message and Folder Class Design

image

When we create a new Message, we will specify the contents of the message but no Folder. Calling save will put a Message in a Folder.

Exercises Section 13.3

Exercise 13.11: What is a destructor? What does the synthesized destructor do? When is a destructor synthesized? When must a class define its own destructor?

Exercise 13.12: Determine whether the NoName class skteched in the exercises on page 481, is likely to need a destructor. If so, implement it.

Exercise 13.13: Determine whether the Employee class, defined in the exercises on page 484, needs a destructor. If so, implement it.

Exercise 13.14: A good way to understand copy-control members and constructors is to define a simple class with these members in which each member prints its name:

image

Write a class like Exmpl, giving it the copy-control members and other constructors. Now write a program using objects of type Exmpl in various ways: pass them as non-reference and reference parameters; dynamically allocate them; put them in containers, and so forth. Studying which constructors and copy-control members are executed and when can be helpful in cementing your understanding of these concepts.

Exercise 13.15: How many destructor calls occur in the following code fragment?

image

When we copy a Message, we’ll copy both the contents of the original message and the set of Folder pointers. We must also add a pointer to this Message to each of the Folders that points to the original Message.

Assigning one Message to another behaves similarly to copying a Message: After the assignment, the contents and set of Folders will be the same. We’ll start by removing the existing left-hand message from the Folders it was in prior to the assignment. Once the old Message is gone, we’ll copy the contents and set of Folders from the right-hand operand into the left. We’ll also have to add a pointer to the left-hand Message to each Folder in this set.

When we destroy a Message, we must update each Folder that points to the Message. Once the Message goes away, those pointers will be no good, so we must remove the pointer to this Message from each Folder in the Message’s own set of Folder pointers.

Looking at this list of operations, we can see that the destructor and the assignment operator share the work of removing messages from the list of Folders that had held a given Message. Similarly, the copy constructor and the assignment operator share the work of adding a Message to a given list of Folders. We’ll define a pair of private utility functions to do these tasks.

The Message Class

Given this design, we can write a fair bit of our Message class:

image

The class defines two data members: contents, which is a string that holds the actual message, and folders, which is a set of pointers to the Folders in which this Message appears.

The constructor takes a single string parameter representing the contents of the message. The constructor stores a copy of the message in contents and (implicitly) initializes the set of Folders to the empty set. This constructor provides a default argument, which is the empty string, so it also serves as the Message default constructor.

The utility functions provide the actions shared among the copy-control members. The put_Msg_in_Folders function adds a copy of its own Message to the Folders that point to the given Message. After this function completes, each Folder that points to the parameter will also point to this Message. This function will be used by both the copy constructor and the assignment operator.

The remove_Msg_from_Folders function is used by the assignment operator and destructor. It removes the pointer to this Message from each of the Folders in the folders member.

Copy Control for the Message Class

When we copy a Message, we have to add the newly created Message to each Folder that holds the Message from which we’re copying. This work is beyond what the synthesized constructor would do for us, so we must define our own copy constructor:

image

The copy constructor initializes the data members of the new object as copies of the members from the old. In addition to these initializations—which the synthesized copy constructor would have done for us—we must also iterate through folders, adding this Message to each Folder in that set. The copy constructor uses the put_Msg_in_Folder function to do this work.

image

When we write our own copy constructor, we must explicitly copy any members that we want copied. An explicitly defined copy constructor copies nothing automatically.

As with any other constructor, if we do not initialize a class member, then that member is initialized using the member’s default constructor. Default initialization in a copy constructor does not use the member’s copy constructor.

The put_Msg_in_Folders Member

put_Msg_in_Folders iterates through the pointers in the folders member of the parameter rhs. These pointers denote each Folder that points to rhs. We need to add a pointer to this Message to each of those Folders.

The function does this work by looping through rhs.folders, calling the Folder member named addMsg. That function will do whatever it takes to add a pointer to this Message to that Folder:

image

The only tricky part in this function is the call to addMsg:

     (*beg)->addMsg(this); // *beg points to a Folder

That call starts with (*beg), which dereferences the iterator. Dereferencing the iterator yields a pointer to a Folder. The expression then applies the arrow operator to the Folder pointer in order to run the addMsg operation. We pass this, which points to the Message we want to add to the Folder.

Message Assignment Operator

Assignment is more complicated than the copy constructor. Like the copy constructor, assignment must assign the contents and update folders to match that of the right-hand operand. It must also add this Message to each of the Folders that points to the rhs. We can use our put_Msg_in_Folders function to do this part of the assignment.

Before copying from the rhs, we must first remove this Message from each of the Folders that currently point to it. We’ll need to iterate through folders, removing the pointer to this Message from each Folder in folders. The function named remove_Msg_from_Folders will do this work.

Given remove_Msg_from_Folders and put_Msg_in_Folders, which do the real work, the assignment operator itself is fairly simple:

image

The assignment operator starts by checking that the left- and right-hand operands are not the same. We do this check for reasons that will become apparent as we walk through the rest of the function. Assuming that the operands are different objects, we call remove_Msg_from_Folders to remove this Message from each of the Folders in the folders member. Once that work is done, we have to assign the contents and folders members from the right-hand operand to this object. Finally, we call put_Msg_in_Folders to add a pointer to this Message in each of the Folders that also point to rhs.

Now that we’ve seen work that remove_Msg_from_Folders does, we can see why we start the assignment operator by checking that the objects are different. Assignment involves obliterating the left-hand operand. Once the members of the left-hand operand are destroyed, those in the right-hand operand are assigned to the corresponding left-hand members. If the objects were the same, then destroying the left-hand members would also destroy the right-hand members!

image

It is crucially important for assignment operators to work correctly, even when an object is assigned to iself. A common way to ensure this behavior is by checking explicitly for self-assignment.

The remove_Msg_from_Folders Member

The implementation of the remove_Msg_from_Folders function is similar to that of put_Msg_in_Folders, except that this time we’ll call remMsg to remove this Message from each Folder pointed to by folders:

image

The Message Destructor

The remaining copy-control function that we must implement is the destructor:

image

Given the remove_Msg_from_Folders function, writing the destructor is trivial. We call that function to clean up folders. The system automatically invokes the string destructor to free contents and the set destructor to clean up the memory used to hold the folders member. Thus, the only work for the Message destructor is to call remove_Msg_from_Folders.

image

The assignment operator often does the same work as is needed in the copy constructor and destructor. In such cases, the common work should be put in private utility functions.

Exercises Section 13.4

Exercise 13.16: Write the Message class as described in this section.

Exercise 13.17: Add functions to the Message class that are analogous to the Folder operations addMsg and remMsg. These functions, which could be named addFldr and remFldr, should take a pointer to a Folder and insert that pointer into folders. These functions can be private because they will be used only in the implementation of the Folder class.

Exercise 13.18: Write the corresponding Folder class. That class should hold a set<Message*> that contains elements that point to Messages.

Exercise 13.19: Add a save and remove operation to the Message and Folder classes. These operations should take a Folder and add (or remove) that Folder to (from) the set of Folders that point to this Message. The operation must also update the Folder to know that it points to this Message, which can be done by calling addMsg or remMsg.

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:

image

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:

image

After the copy, the pointers in ptr1 and ptr2 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.

image

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:

image

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:

image

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:

image

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:

image

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:

image

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:

image

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:

image

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*.

image

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

image

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:

image

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.

image

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:

image

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.

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:

image

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:

image

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

image

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

(a) The copy constructor and destructor but no assignment operator.

(b) The copy constructor and assignment operator but no destructor.

(c) 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.

image

Chapter Summary

In addition to defining the operations on objects of its type, a class also defines what it means to copy, assign, or destroy objects of the type. Special member functions—the copy constructor, the assignment operator, and the destructor— define these operations. Collectively these operations are referred to as the “copy control” functions.

If a class does not define one or more of these operations, the compiler will define them automatically. The synthesized operations perform memberwise initialization, assignment, or destruction: Taking each member in turn, the synthesized operation does whatever is appropriate to the member’s type to copy, assign, or destroy that member. If the member is a class type, the synthesized operation calls the corresponding operation for that class (e.g., the copy constructor calls the member’s copy constructor, the destructor calls its destructor, etc.). If the member is a built-in type or a pointer, the member is copied or assigned directly; the destructor does nothing to destroy members of built-in or pointer type. If the member is an array, the elements in the array are copied, assigned, or destroyed in a manner appropriate to the element type.

Unlike the copy constructor and assignment operator, the synthesized destructor is created and run, regardless of whether the class defines its own destructor. The synthesized destructor is run after the class-defined destructor, if there is one, completes.

image

The hardest part of defining the copy-control functions is often simply recognizing that they are necessary.

Classes that allocate memory or other resources almost always require that the class define the copy-control members to manage the allocated resource. If a class needs a destructor, then it almost surely needs to define the copy constructor and assignment operator as well.

Defined Terms

assignment operator

The assignment operator can be overloaded to define what it means to assign one object of a class type to another of the same type. The assignment operator must be a member of its class and should return a reference to its object. The compiler synthesizes the assignment operator if the class does not explicitly define one.

copy constructor

Constructor that initializes a new object as a copy of another object of the same type. The copy constructor is applied implicitly to pass objects to or from a function by value. If we do not define the copy constructor, the compiler synthesizes one for us.

copy control

Special members that control what happens when objects of class type are copied, assigned, and destroyed. The compiler synthesizes appropriate definitions for these operations if the class does not otherwise define them.

destructor

Special member function that cleans up an object when the object goes out of scope or is deleted. The compiler automatically destroys each member. Members of class type are destroyed by invoking their destructor; no explicit work is done to destroy members of built-in or compound type. In particular, the object pointed to by a pointer member is not deleted by the automatic work done by the destructor.

memberwise assignment

Term used to describe how the synthesized assignment operator works. The assignment operator assigns, member by member, from the old object to the new. Members of built-in or compound type are assigned directly. Those that are of class type are assigned by using the member’s assignment operator.

memberwise initialization

Term used to described how the synthesized copy constructor works. The constructor copies, member by member, from the old object to the new. Members of built-in or compound type are copied directly. Those that are of class type are copied by using the member’s copy constructor.

overloaded operator

Function that redefines one of the C++ operators to operate on object(s) of class type. This chapter showed how to define the assignment operator; Chapter 14 covers overloaded operators in more detail.

reference count

Synonym for use count.

Rule of Three

Shorthand for the rule of thumb that if a class needs a nontrivial destructor then it almost surely also needs to define its own copy constructor and an assignment operator.

smart pointer

A class that behaves like a pointer but provides other functionality as well. One common form of smart pointer takes a pointer to a dynamically allocated object and assumes responsibility for deleting that object. The user allocates the object, but the smart pointer class deletes it. Smart pointer classes require that the class implement the copy-control members to manage a pointer to the shared object. That object is deleted only when the last smart pointer pointing to it is destroyed. Use counting is the most popular way to implement smart pointer classes.

synthesized assignment operator

A version of the assignment operator created (synthesized) by the compiler for classes that do not explicitly define one. The synthesized assignment operator memberwise assigns the right-hand operand to the left.

synthesized copy constructor

The copy constructor created (synthesized) by the compiler for classes that do not explicitly define the copy constructor. The synthesized copy constructor memberwise initializes the new object from the existing one.

use count

Programming technique used in copy-control members. A use count is stored along with a shared object. A separate class is created that points to the shared object and manages the use count. The constructors, other than the copy constructor, set the state of the shared object and set the use count to one. Each time a new copy is made—either in the copy constructor or the assignment operator—the use count is incremented. When an object is destroyed— either in the destructor or as the left-hand side of the assignment operator—the use count is decremented. The assignment operator and the destructor check whether the decremented use count has gone to zero and, if so, they destroy the object.

value semantics

Description of the copy-control behavior of classes that mimic the way arithmetic types are copied. Copies of valuelike objects are independent: Changes made to a copy have no effect on the original object. A valuelike class that has a pointer member must define its own copy-control members. The copy-control operations copy the object to which the pointer points. Valuelike classes that contain only other valuelike classes or built-in types often can rely on the synthesized copy-control members.

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

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