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
FundamentalA 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 explicit
(§ 7.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.
INFO
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 StrBlobPtr
s?
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 string
(§ 12.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
FundamentalJust 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.
TIP
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
}
INFO
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 StrBlobPtr
s?
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
FundamentalThe 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.
INFO
The implicit destruction of a member of built-in pointer type does notdelete
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.
INFO
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
FundamentalAs 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.
INFO
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.
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.
INFO
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
C++11We can explicitly ask the compiler to generate the synthesized versions of the copy-control members by defining them as = default
(§ 7.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.
INFO
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
TIP
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
C++11Under 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
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.
INFO
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.
TIP
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
.
INFO
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.