Team LiB
Previous Section Next Section

13.1. Copy, Assign, and Destroy

 

We’ll start by covering the most basic operations, which are the copy constructor, copy-assignment operator, and destructor. We’ll cover the move operations (which were introduced by the new standard) in § 13.6 (p. 531).

 

13.1.1. The Copy Constructor

 
Image

A constructor is the copy constructor if its first parameter is a reference to the class type and any additional parameters have default values:

 

 

class Foo {
public:
   Foo();             // default constructor
   Foo(const Foo&);   // copy constructor
   // ...
};

 

For reasons we’ll explain shortly, the first parameter must be a reference type. That parameter is almost always a reference to const, although we can define the copy constructor to take a reference to nonconst. The copy constructor is used implicitly in several circumstances. Hence, the copy constructor usually should not be explicit7.5.4, p. 296).

 
The Synthesized Copy Constructor
 

When we do not define a copy constructor for a class, the compiler synthesizes one for us. Unlike the synthesized default constructor (§ 7.1.4, p. 262), a copy constructor is synthesized even if we define other constructors.

 

As we’ll see in § 13.1.6 (p. 508), the synthesized copy constructor for some classes prevents us from copying objects of that class type. Otherwise, the synthesized copy constructor memberwise copies the members of its argument into the object being created (§ 7.1.5, p. 267). The compiler copies each nonstatic member in turn from the given object into the one being created.

 

The type of each member determines how that member is copied: Members of class type are copied by the copy constructor for that class; members of built-in type are copied directly. Although we cannot directly copy an array (§ 3.5.1, p. 114), the synthesized copy constructor copies members of array type by copying each element. Elements of class type are copied by using the elements’ copy constructor.

 

As an example, the synthesized copy constructor for our Sales_data class is equivalent to:

 

 

class Sales_data {
public:
    // other members and constructors as before
    // declaration equivalent to the synthesized copy constructor
    Sales_data(const Sales_data&);
private:
    std::string bookNo;
    int units_sold = 0;
    double revenue = 0.0;
};
// equivalent to the copy constructor that would be synthesized for Sales_data
Sales_data::Sales_data(const Sales_data &orig):
    bookNo(orig.bookNo),         // uses the string copy constructor
    units_sold(orig.units_sold), // copies orig.units_sold
    revenue(orig.revenue)        // copies orig.revenue
    {    }                       // empty body

 
Copy Initialization
 

We are now in a position to fully understand the differences between direct initialization and copy initialization (§ 3.2.1, p. 84):

 

 

string dots(10, '.');               // direct initialization
string s(dots);                     // direct initialization
string s2 = dots;                   // copy initialization
string null_book = "9-999-99999-9"; // copy initialization
string nines = string(100, '9');    // copy initialization

 

When we use direct initialization, we are asking the compiler to use ordinary function matching (§ 6.4, p. 233) to select the constructor that best matches the arguments we provide. When we use copy initialization, we are asking the compiler to copy the right-hand operand into the object being created, converting that operand if necessary (§ 7.5.4, p. 294).

 

Copy initialization ordinarily uses the copy constructor. However, as we’ll see in § 13.6.2 (p. 534), if a class has a move constructor, then copy initialization sometimes uses the move constructor instead of the copy constructor. For now, what’s useful to know is when copy initialization happens and that copy initialization requires either the copy constructor or the move constructor.

 

Copy initialization happens not only when we define variables using an =, but also when we

 

• Pass an object as an argument to a parameter of nonreference type

 

• Return an object from a function that has a nonreference return type

 

• Brace initialize the elements in an array or the members of an aggregate class (§ 7.5.5, p. 298)

 

Some class types also use copy initialization for the objects they allocate. For example, the library containers copy initialize their elements when we initialize the container, or when we call an insert or push member (§ 9.3.1, p. 342). By contrast, elements created by an emplace member are direct initialized (§ 9.3.1, p. 345).

 
Parameters and Return Values
 

During a function call, parameters that have a nonreference type are copy initialized (§ 6.2.1, p. 209). Similarly, when a function has a nonreference return type, the return value is used to copy initialize the result of the call operator at the call site (§ 6.3.2, p. 224).

 

The fact that the copy constructor is used to initialize nonreference parameters of class type explains why the copy constructor’s own parameter must be a reference. If that parameter were not a reference, then the call would never succeed—to call the copy constructor, we’d need to use the copy constructor to copy the argument, but to copy the argument, we’d need to call the copy constructor, and so on indefinitely.

 
Constraints on Copy Initialization
 

As we’ve seen, whether we use copy or direct initialization matters if we use an initializer that requires conversion by an explicit constructor (§ 7.5.4, p. 296):

 

 

vector<int> v1(10);  // ok: direct initialization
vector<int> v2 = 10; // error: constructor that takes a size is explicit
void f(vector<int>); // f's parameter is copy initialized
f(10); // error: can't use an explicit constructor to copy an argument
f(vector<int>(10));  // ok: directly construct a temporary vector from an int

 

Directly initializing v1 is fine, but the seemingly equivalent copy initialization of v2 is an error, because the vector constructor that takes a single size parameter is explicit. For the same reasons that we cannot copy initialize v2, we cannot implicitly use an explicit constructor when we pass an argument or return a value from a function. If we want to use an explicit constructor, we must do so explicitly, as in the last line of the example above.

 
The Compiler Can Bypass the Copy Constructor
 

During copy initialization, the compiler is permitted (but not obligated) to skip the copy/move constructor and create the object directly. That is, the compiler is permitted to rewrite

 

 

string null_book = "9-999-99999-9"; // copy initialization

 

into

 

 

string null_book("9-999-99999-9"); // compiler omits the copy constructor

 

However, even if the compiler omits the call to the copy/move constructor, the copy/move constructor must exist and must be accessible (e.g., not private) at that point in the program.

 

Exercises Section 13.1.1

 

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

Exercise 13.2: Explain why the following declaration is illegal:

 

Sales_data::Sales_data(Sales_data rhs);

 

Exercise 13.3: What happens when we copy a StrBlob? What about StrBlobPtrs?

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

 


Point global;
Point foo_bar(Point arg)
{
    Point local = arg, *heap = new Point(global);
    *heap = local;
    Point pa[ 4 ] = { local, *heap };
    return *heap;
}

 

Exercise 13.5: Given the following sketch of a class, write a copy constructor that copies all the members. Your constructor should dynamically allocate a new string12.1.2, p. 458) and copy the object to which ps points, rather than copying ps itself.

 

 


class HasPtr {
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
private:
    std::string *ps;
    int    i;
};

 

 

13.1.2. The Copy-Assignment Operator

 
Image

Just as a class controls how objects of that class are initialized, it also controls how objects of its class are assigned:

 

 

Sales_data trans, accum;
trans = accum; // uses the Sales_data copy-assignment operator

 

As with the copy constructor, the compiler synthesizes a copy-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, the assignment operator is a function named operator=. Like any other function, an operator function has a return type and a parameter list.

 

The parameters in an overloaded operator represent the operands of the operator. Some operators, assignment among them, must be defined as member functions. When an operator is a member function, the left-hand operand is bound to the implicit this parameter (§ 7.1.2, p. 257). The right-hand operand in a binary operator, such as assignment, is passed as an explicit parameter.

 

The copy-assignment operator takes an argument of the same type as the class:

 

 

class Foo {
public:
    Foo& operator=(const Foo&); // assignment operator
   // ...
};

 

To be consistent with assignment for the built-in types (§ 4.4, p. 145), assignment operators usually return a reference to their left-hand operand. It is also worth noting that the library generally requires that types stored in a container have assignment operators that return a reference to the left-hand operand.

 

Image Best Practices

Assignment operators ordinarily should return a reference to their left-hand operand.

 

 
The Synthesized Copy-Assignment Operator
 

Just as it does for the copy constructor, the compiler generates a synthesized copy-assignment operator for a class if the class does not define its own. Analogously to the copy constructor, for some classes the synthesized copy-assignment operator disallows assignment (§ 13.1.6, p. 508). Otherwise, it assigns each nonstatic member of the right-hand object to the corresponding member of the left-hand object using the copy-assignment operator for the type of that member. Array members are assigned by assigning each element of the array. The synthesized copy-assignment operator returns a reference to its left-hand object.

 

As an example, the following is equivalent to the synthesized Sales_data copy-assignment operator:

 

 

// equivalent to the synthesized copy-assignment operator
Sales_data&
Sales_data::operator=(const Sales_data &rhs)
{
    bookNo = rhs.bookNo;          // calls the string::operator=
    units_sold = rhs.units_sold;  // uses the built-in int assignment
    revenue = rhs.revenue;        // uses the built-in double assignment
    return *this;                 // return a reference to this object
}

 

Exercises Section 13.1.2

 

Exercise 13.6: What is a copy-assignment operator? When is this operator used? What does the synthesized copy-assignment operator do? When is it synthesized?

Exercise 13.7: What happens when we assign one StrBlob to another? What about StrBlobPtrs?

Exercise 13.8: Write the assignment operator for the HasPtr class from exercise 13.5 in § 13.1.1 (p. 499). As with the copy constructor, your assignment operator should copy the object to which ps points.

 

 

13.1.3. The Destructor

 
Image

The destructor operates inversely to the constructors: Constructors initialize the nonstatic data members of an object and may do other work; destructors do whatever work is needed to free the resources used by an object and destroy the nonstatic data members of the object.

 

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:

 

class Foo {
public:
    ~Foo();    // destructor
   // ...
};

 

Because it takes no parameters, it cannot be overloaded. There is always only one destructor for a given class.

 
What a Destructor Does
 

Just as a constructor has an initialization part and a function body (§ 7.5.1, p. 288), a destructor has a function body and a destruction part. In a constructor, members are initialized before the function body is executed, and members are initialized in the same order as they appear in the class. In a destructor, the function body is executed first and then the members are destroyed. Members are destroyed in reverse order from the order in which they were initialized.

 

The function body of a destructor does whatever operations the class designer wishes to have executed subsequent to the last use of an object. Typically, the destructor frees resources an object allocated during its lifetime.

 

In a destructor, there is nothing akin to the constructor initializer list to control how members are destroyed; the destruction part is implicit. What happens when a member is destroyed depends on the type of the member. Members of class type are destroyed by running the member’s own destructor. The built-in types do not have destructors, so nothing is done to destroy members of built-in type.

 

Image Note

The implicit destruction of a member of built-in pointer type does not delete the object to which that pointer points.

 

 

Unlike ordinary pointers, the smart pointers (§ 12.1.1, p. 452) are class types and have destructors. As a result, unlike ordinary pointers, members that are smart pointers are automatically destroyed during the destruction phase.

 
When a Destructor Is Called
 

The destructor is used automatically whenever an object of its type is destroyed:

 

• Variables are destroyed when they go out of scope.

 

• Members of an object are destroyed when the object of which they are a part is destroyed.

 

• Elements in a container—whether a library container or an array—are destroyed when the container is destroyed.

 

• Dynamically allocated objects are destroyed when the delete operator is applied to a pointer to the object (§ 12.1.2, p. 460).

 

• Temporary objects are destroyed at the end of the full expression in which the temporary was created.

 

Because destructors are run automatically, our programs can allocate resources and (usually) not worry about when those resources are released.

 

For example, the following fragment defines four Sales_data objects:

 

 

{ // new scope
    // p and p2 point to dynamically allocated objects
    Sales_data  *p = new Sales_data;     // p is a built-in pointer
    auto p2 = make_shared<Sales_data>(); // p2 is a shared_ptr
    Sales_data item(*p);     // copy constructor copies *p into item
    vector<Sales_data> vec;  // local object
    vec.push_back(*p2);      // copies the object to which p2 points
    delete p;                // destructor called on the object pointed to by p
} // exit local scope; destructor called on item, p2, and vec
  // destroying p2 decrements its use count; if the count goes to 0, the object is freed
  // destroying vec destroys the elements in vec

 

Each of these objects contains a string member, which allocates dynamic memory to contain the characters in its bookNo member. However, the only memory our code has to manage directly is the object we directly allocated. Our code directly frees only the dynamically allocated object bound to p.

 

The other Sales_data objects are automatically destroyed when they go out of scope. When the block ends, vec, p2, and item all go out of scope, which means that the vector, shared_ptr, and Sales_data destructors will be run on those objects, respectively. The vector destructor will destroy the element we pushed onto vec. The shared_ptr destructor will decrement the reference count of the object to which p2 points. In this example, that count will go to zero, so the shared_ptr destructor will delete the Sales_data object that p2 allocated.

 

In all cases, the Sales_data destructor implicitly destroys the bookNo member. Destroying bookNo runs the string destructor, which frees the memory used to store the ISBN.

 

Image Note

The destructor is not run when a reference or a pointer to an object goes out of scope.

 

 
The Synthesized Destructor
 

The compiler defines a synthesized destructor for any class that does not define its own destructor. As with the copy constructor and the copy-assignment operator, for some classes, the synthesized destructor is defined to disallow objects of the type from being destroyed (§ 13.1.6, p. 508). Otherwise, the synthesized destructor has an empty function body.

 

For example, the synthesized Sales_data destructor is equivalent to:

 

 

class Sales_data {
public:
   // no work to do other than destroying the members, which happens automatically
    ~Sales_data() { }
   // other members as before
};

 

The members are automatically destroyed after the (empty) destructor body is run. In particular, the string destructor will be run to free the memory used by the bookNo member.

 

It is important to realize that the destructor body does not directly destroy the members themselves. Members are destroyed as part of the implicit destruction phase that follows the destructor body. A destructor body executes in addition to the memberwise destruction that takes place as part of destroying an object.

 

13.1.4. The Rule of Three/Five

 
Image

As we’ve seen, there are three basic operations to control copies of class objects: the copy constructor, copy-assignment operator, and destructor. Moreover, as we’ll see in § 13.6 (p. 531), under the new standard, a class can also define a move constructor and move-assignment operator.

 

Exercises Section 13.1.3

 

Exercise 13.9: What is a destructor? What does the synthesized destructor do? When is a destructor synthesized?

Exercise 13.10: What happens when a StrBlob object is destroyed? What about a StrBlobPtr?

Exercise 13.11: Add a destructor to your HasPtr class from the previous exercises.

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

 


bool fcn(const Sales_data *trans, Sales_data accum)
{
    Sales_data item1(*trans), item2(accum);
    return item1.isbn() != item2.isbn();
}

 

Exercise 13.13: 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:

 


struct X {
    X() {std::cout << "X()" << std::endl;}
    X(const X&) {std::cout << "X(const X&)" << std::endl;}
};

 

Add the copy-assignment operator and destructor to X and write a program using X objects in various ways: Pass them as nonreference and reference parameters; dynamically allocate them; put them in containers; and so forth. Study the output until you are certain you understand when and why each copy-control member is used. As you read the output, remember that the compiler can omit calls to the copy constructor.

 

 

There is no requirement that we define all of these operations: We can define one or two of them without having to define all of them. However, ordinarily these operations should be thought of as a unit. In general, it is unusual to need one without needing to define them all.

 
Classes That Need Destructors Need Copy and Assignment
 

One rule of thumb to use when you decide whether a class needs to define its own versions of the copy-control members is to decide first whether the class needs a destructor. Often, the need for a destructor is more obvious than the need for the copy constructor or assignment operator. If the class needs a destructor, it almost surely needs a copy constructor and copy-assignment operator as well.

 

The HasPtr class that we have used in the exercises is a good example (§ 13.1.1, p. 499). That class allocates dynamic memory in its constructor. The synthesized destructor will not delete a data member that is a pointer. Therefore, this class needs to define a destructor to free the memory allocated by its constructor.

 

What may be less clear—but what our rule of thumb tells us—is that HasPtr also needs a copy constructor and copy-assignment operator.

 

Consider what would happen if we gave HasPtr a destructor but used the synthesized versions of the copy constructor and copy-assignment operator:

 

 

class HasPtr {
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
    ~HasPtr() { delete ps; }
    // WRONG: HasPtr needs a copy constructor and copy-assignment operator
    // other members as before
};

 

In this version of the class, the memory allocated in the constructor will be freed when a HasPtr object is destroyed. Unfortunately, we have introduced a serious bug! This version of the class uses the synthesized versions of copy and assignment. Those functions copy the pointer member, meaning that multiple HasPtr objects may be pointing to the same memory:

 

 

HasPtr f(HasPtr hp)  // HasPtr passed by value, so it is copied
{
    HasPtr ret = hp; // copies the given HasPtr
    // process ret
    return ret;      // ret and hp are destroyed
}

 

When f returns, both hp and ret are destroyed and the HasPtr destructor is run on each of these objects. That destructor will delete the pointer member in ret and in hp. But these objects contain the same pointer value. This code will delete that pointer twice, which is an error (§ 12.1.2, p. 462). What happens is undefined.

 

In addition, the caller of f may still be using the object that was passed to f:

 

 

HasPtr p("some values");
f(p);        // when f completes, the memory to which p.ps points is freed
HasPtr q(p); // now both p and q point to invalid memory!

 

The memory to which p (and q) points is no longer valid. It was returned to the system when hp (or ret!) was destroyed.

 

Image Tip

If a class needs a destructor, it almost surely also needs the copy-assignment operator and a copy constructor.

 

 
Classes That Need Copy Need Assignment, and Vice Versa
 

Although many classes need to define all of (or none of) the copy-control members, some classes have work that needs to be done to copy or assign objects but has no need for the destructor.

 

As an example, consider a class that gives each object its own, unique serial number. Such a class would need a copy constructor to generate a new, distinct serial number for the object being created. That constructor would copy all the other data members from the given object. This class would also need its own copy-assignment operator to avoid assigning to the serial number of the left-hand object. However, this class would have no need for a destructor.

 

This example gives rise to a second rule of thumb: If a class needs a copy constructor, it almost surely needs a copy-assignment operator. And vice versa—if the class needs an assignment operator, it almost surely needs a copy constructor as well. Nevertheless, needing either the copy constructor or the copy-assignment operator does not (necessarily) indicate the need for a destructor.

 

Exercises Section 13.1.4

 

Exercise 13.14: Assume that numbered is a class with a default constructor that generates a unique serial number for each object, which is stored in a data member named mysn. Assuming numbered uses the synthesized copy-control members and given the following function:

 


void f (numbered s) { cout << s.mysn << endl; }

 

what output does the following code produce?

 

 


numbered a, b = a, c = b;
f(a); f(b); f(c);

 

Exercise 13.15: Assume numbered has a copy constructor that generates a new serial number. Does that change the output of the calls in the previous exercise? If so, why? What output gets generated?

Exercise 13.16: What if the parameter in f were const numbered&? Does that change the output? If so, why? What output gets generated?

Exercise 13.17: Write versions of numbered and f corresponding to the previous three exercises and check whether you correctly predicted the output.


 

13.1.5. Using = default

 
Image

We can explicitly ask the compiler to generate the synthesized versions of the copy-control members by defining them as = default7.1.4, p. 264):

 

 

class Sales_data {
public:
    // copy control; use defaults
    Sales_data() = default;
    Sales_data(const Sales_data&) = default;
    Sales_data& operator=(const Sales_data &);
    ~Sales_data() = default;
    // other members as before
};
Sales_data& Sales_data::operator=(const Sales_data&) = default;

 

When we specify = default on the declaration of the member inside the class body, the synthesized function is implicitly inline (just as is any other member function defined in the body of the class). If we do not want the synthesized member to be an inline function, we can specify = default on the member’s definition, as we do in the definition of the copy-assignment operator.

 

Image Note

We can use = default only on member functions that have a synthesized version (i.e., the default constructor or a copy-control member).

 

 

13.1.6. Preventing Copies

 

Image Best Practices

Most classes should define—either implicitly or explicitly—the default and copy constructors and the copy-assignment operator.

 

 

Although most classes should (and generally do) define a copy constructor and a copy-assignment operator, for some classes, there really is no sensible meaning for these operations. In such cases, the class must be defined so as to prevent copies or assignments from being made. For example, the iostream classes prevent copying to avoid letting multiple objects write to or read from the same IO buffer. It might seem that we could prevent copies by not defining the copy-control members. However, this strategy doesn’t work: If our class doesn’t define these operations, the compiler will synthesize them.

 
Defining a Function as Deleted
 
Image

Under the new standard, we can prevent copies by defining the copy constructor and copy-assignment operator as deleted functions. A deleted function is one that is declared but may not be used in any other way. We indicate that we want to define a function as deleted by following its parameter list with = delete:

 

 

struct NoCopy {
    NoCopy() = default;    // use the synthesized default constructor
    NoCopy(const NoCopy&) = delete;            // no copy
    NoCopy &operator=(const NoCopy&) = delete; // no assignment
    ~NoCopy() = default;   // use the synthesized destructor
    // other members
};

 

The = delete signals to the compiler (and to readers of our code) that we are intentionally not defining these members.

 

Unlike = default, = delete must appear on the first declaration of a deleted function. This difference follows logically from the meaning of these declarations. A defaulted member affects only what code the compiler generates; hence the = default is not needed until the compiler generates code. On the other hand, the compiler needs to know that a function is deleted in order to prohibit operations that attempt to use it.

 

Also unlike = default, we can specify = delete on any function (we can use = default only on the default constructor or a copy-control member that the compiler can synthesize). Although the primary use of deleted functions is to suppress the copy-control members, deleted functions are sometimes also useful when we want to guide the function-matching process.

 
The Destructor Should Not be a Deleted Member
 

It is worth noting that we did not delete the destructor. If the destructor is deleted, then there is no way to destroy objects of that type. The compiler will not let us define variables or create temporaries of a type that has a deleted destructor. Moreover, we cannot define variables or temporaries of a class that has a member whose type has a deleted destructor. If a member has a deleted destructor, then that member cannot be destroyed. If a member can’t be destroyed, the object as a whole can’t be destroyed.

 

Although we cannot define variables or members of such types, we can dynamically allocate objects with a deleted destructor. However, we cannot free them:

 

 

struct NoDtor {
    NoDtor() =  default;  // use the synthesized default constructor
    ~NoDtor() = delete;  // we can't destroy objects of type NoDtor
};
NoDtor nd;  // error: NoDtor destructor is deleted
NoDtor *p = new NoDtor();   // ok: but we can't delete p
delete p; // error: NoDtor destructor is deleted

 

Image Warning

It is not possible to define an object or delete a pointer to a dynamically allocated object of a type with a deleted destructor.

 

 
The Copy-Control Members May Be Synthesized as Deleted
 

As we’ve seen, if we do not define the copy-control members, the compiler defines them for us. Similarly, if a class defines no constructors, the compiler synthesizes a default constructor for that class (§ 7.1.4, p. 262). For some classes, the compiler defines these synthesized members as deleted functions:

 

• The synthesized destructor is defined as deleted if the class has a member whose own destructor is deleted or is inaccessible (e.g., private).

 

• The synthesized copy constructor is defined as deleted if the class has a member whose own copy constructor is deleted or inaccessible. It is also deleted if the class has a member with a deleted or inaccessible destructor.

 

• The synthesized copy-assignment operator is defined as deleted if a member has a deleted or inaccessible copy-assignment operator, or if the class has a const or reference member.

 

• The synthesized default constructor is defined as deleted if the class has a member with a deleted or inaccessible destructor; or has a reference member that does not have an in-class initializer (§ 2.6.1, p. 73); or has a const member whose type does not explicitly define a default constructor and that member does not have an in-class initializer.

 

In essence, these rules mean that if a class has a data member that cannot be default constructed, copied, assigned, or destroyed, then the corresponding member will be a deleted function.

 

It may be surprising that a member that has a deleted or inaccessible destructor causes the synthesized default and copy constructors to be defined as deleted. The reason for this rule is that without it, we could create objects that we could not destroy.

 

It should not be surprising that the compiler will not synthesize a default constructor for a class with a reference member or a const member that cannot be default constructed. Nor should it be surprising that a class with a const member cannot use the synthesized copy-assignment operator: After all, that operator attempts to assign to every member. It is not possible to assign a new value to a const object.

 

Although we can assign a new value to a reference, doing so changes the value of the object to which the reference refers. If the copy-assignment operator were synthesized for such classes, the left-hand operand would continue to refer to the same object as it did before the assignment. It would not refer to the same object as the right-hand operand. Because this behavior is unlikely to be desired, the synthesized copy-assignment operator is defined as deleted if the class has a reference member.

 

We’ll see in § 13.6.2 (p. 539), § 15.7.2 (p. 624), and § 19.6 (p. 849) that there are other aspects of a class that can cause its copy members to be defined as deleted.

 

Image Note

In essence, the copy-control members are synthesized as deleted when it is impossible to copy, assign, or destroy a member of the class.

 

 
private Copy Control
 

Prior to the new standard, classes prevented copies by declaring their copy constructor and copy-assignment operator as private:

 

 

class PrivateCopy {
    // no access specifier; following members are private by default; see § 7.2 (p. 268)
    // copy control is private and so is inaccessible to ordinary user code
    PrivateCopy(const PrivateCopy&);
    PrivateCopy &operator=(const PrivateCopy&);
    // other members
public:
    PrivateCopy() = default; // use the synthesized default constructor
    ~PrivateCopy(); // users can define objects of this type but not copy them
};

 

Because the destructor is public, users will be able to define PrivateCopy objects. However, because the copy constructor and copy-assignment operator are private, user code will not be able to copy such objects. However, friends and members of the class can still make copies. To prevent copies by friends and members, we declare these members as private but do not define them.

 

With one exception, which we’ll cover in § 15.2.1 (p. 594), it is legal to declare, but not define, a member function (§ 6.1.2, p. 206). An 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: User code that tries to make a copy will be flagged as an error at compile time; copies made in member functions or friends will result in an error at link time.

 

Image Best Practices

Classes that want to prevent copying should define their copy constructor and copy-assignment operators using = delete rather than making those members private.

 

 

Exercises Section 13.1.6

 

Exercise 13.18: Define an Employee class that contains an employee name and a unique employee identifier. Give the class a default constructor and a constructor that takes a string representing the employee’s name. Each constructor should generate a unique ID by incrementing a static data member.

Exercise 13.19: Does your Employee class need to define its own versions of the copy-control members? If so, why? If not, why not? Implement whatever copy-control members you think Employee needs.

Exercise 13.20: Explain what happens when we copy, assign, or destroy objects of our TextQuery and QueryResult classes from § 12.3 (p. 484).

 

Exercise 13.21: Do you think the TextQuery and QueryResult classes need to define their own versions of the copy-control members? If so, why? If not, why not? Implement whichever copy-control operations you think these classes require.


 
Team LiB
Previous Section Next Section