SoFunction
Updated on 2025-04-13

Detailed explanation of Primer copy, assignment and destruction in C++

Copy control and resource management

In this chapter, we will learn that classes can define constructors that control what to do when creating an object of this type. In this chapter, we will also learn how classes control what to do when objects of that type are copied, assigned, moved, or destroyed. Classes control these operations through some special member functions, including: copy constructor, move constructor, copy assignment operator, move assignment operator and destructor.

When defining a class, we explicitly or implicitly specify what to do when objects of this type are copied, moved, assigned, and destroyed. A class controls these operations by defining five special member functions, including: copy constructor, copy-assignment operator, move constructor, move-assignment operator and destructor. The copy and move constructor defines what to do when the object is initialized with another object of the same type. The copy and move assignment operators define what to do when you assign an object to another object of the same type. The destructor defines what to do when an object of this type is destroyed. We call these operations copy control operations.

If a class does not define all these copy control members, the compiler will automatically define the missing operations for it. Therefore, many classes ignore these copy control operations. However, for some classes, relying on the default definition of these operations can cause disaster. Generally, the most difficult part of implementing copy control operations is to first recognize when these operations need to be defined.

Copy control operations are a necessary part when defining any C++ class. For beginners of C++ programmers, it is often troubled that they must define what to do when object copying, moving, and decoding objects are destroyed. This trouble is complicated because if we don't explicitly define these operations, the compiler will also define it for us, but the compiler-defined version behavior may not be what we think.

Copy, assignment and destruction

We will start with the most basic operations: copying the constructor, copying the assignment operator and destructor.

Copy constructor

If the first parameter of a constructor is a reference to its own class type, and any additional parameters have default values, this constructor is a copy constructor.

class Foo{
    public:
    Foo();           //Default constructor    Foo(const Foo&); // Copy constructor}

The first parameter of the copy constructor must be a reference type, and we explain the reason later. Although we can define a copy constructor that accepts non-const references, this parameter is almost always a const reference. The copy constructor is used implicitly in several cases. Therefore, copy constructors should not usually be explicit.

Synthetic copy constructor

If we do not define a copy constructor for a class, the compiler will define one for us. Unlike synthesising default constructors, the compiler synthesizes a copy constructor for us even if we define other constructors.

For some classes, the synthesized copy constructor is used to prevent us from copying objects of this class type. Generally, the synthetic copy constructor will copy members of its parameters to the object being created one by one. The compiler copies each non-static member from the given object to the object being created in turn.

The type of each member determines how it is copied: for members of a class type, their copy constructor will be used to copy; for members of the built-in type, they will be copied directly. Although we cannot copy an array directly, the synthetic copy constructor copies a member of an array type element by element. If the array element is a class type, the copy constructor of the element is used to copy.

As an example, the synthetic copy constructor of our sales_data class is equivalent to:

class Sales_data
public:
//Definition of other members and constructors, as before//The statement of the composite copy constructor is equivalent to the statement of the Bei constructorSales_data(const Sales_data&);
private:
std::string bookKNo;
int unit_sold = 0;
double revenue=0.0;
//The synthesis of the finger-bearing constructor with Sales_data is equivalent toSales_data::Sales_data(const Sales_data&orig):
bookNo(),//Use the finger-bearing constructor of stritngunits_sold(orig.units_sold),//Refer to orig.units_soldrevenue()//copy{}//empty function body

Copy Initialization

Now we can fully understand the difference between direct initialization and copy initialization:

string dots(10,'.');//Direct initializationstring s(dots)}//Direct initializationstring s2=dots;//Copy initializationstring null_book="9-~999-99999-9";//Copy initializationstring nines=string(100,'9'); //Copy Initialization

When using direct initialization, we are actually asking the compiler to use normal function matching to select the constructor that best matches the parameters we provide. When we use copy initialization, we ask the compiler to copy the right-hand operation object to the object being created and type conversion is required if necessary.

Copy initialization is usually done using a copy constructor. However, if a class has a move constructor, copy initialization is sometimes done using a move constructor rather than a copy constructor. But now, we just need to understand when copy initialization occurs and that copy initialization is done by copy constructor or move constructor.

Copy initialization will not only happen when we define variables with =, but also in the following cases

  • Pass an object as an actual parameter to a non-referenced formal parameter
  • Return an object from a function that returns a type of non-reference type
  • Initialize an element in an array or a member in an aggregate class with a list of curly braces

Certain class types also use copy initialization for the objects they are allocated. For example, when we initialize a standard library container or call its insert or push member, the container will copy and initialize its elements. In contrast, all elements created with emplace members are initialized directly.

Copy initialization restrictions

As mentioned earlier, if we use the initialization value to require type conversion through an explicit constructor, then it is not irrelevant to use copy initialization to be directly initialized:

vector<int>v1(10);//Correct, direct initializationvector<int>v2=10;//Error: The constructor that accepts size parameters is explicitvoid(vector<int>);// parameters are initializedf(10);//Error: You cannot copy an actual parameter with an explicit constructorf(vector<int>(10));//correct:From oneintConstruct a temporaryvector

It is legal to initialize v1 directly, but it seems that copy initialization v2 equivalent is wrong, because the vector's constructor that accepts a single-size parameter is explicit. For the same reason, we cannot implicitly use an explicit constructor when passing an argument or returning a value from a function. If we want to use an explicit constructor, we must use it explicitly, like the last line in this code. The compiler can bypass copy constructors

During copy initialization, the compiler can (but does not have to) skip copy/move constructors and create objects directly. That is, the compiler is allowed to use the following code

string null_book="9-999-99999-9";//Copy Initialization

Rewrite as

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

However, even if the compiler skips the copy/move constructor, at this program point, the copy/move constructor must be present and accessible (eg, not private).

Copy assignment operator

Just as a class controls how its objects are initialized, a class can also control how its objects are assigned:

Sales_data trans,accum;
trans=accum;//useSales_datacopy assignment operator

Like the copy constructor, if the class does not define its own copy assignment operator, the compiler will synthesize one for it.

Overload assignment operators

Before introducing the synthetic assignment operator, we need to know a little about overloaded operators.

An overloaded operator is essentially a function, and its name is composed of the operator keyword followed by a symbol representing the operator to be defined. Therefore, the assignment operator is a function called operator=. Similar to any other function,
The operator function also has a return type and a parameter list.

The parameters of the overloaded operator represent the operator's operation object. Some operators, including assignment operators, must be defined as member functions. If an operator is a member function, its left-hand operation object is bound to the implicit this parameter. For a binary operator, such as an assignment operator, the right-hand operation object is passed as an explicit parameter.

The copy assignment operator accepts a parameter of the same type as its class:

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

To be consistent with assignments of built-in types, assignment operators usually return a reference to the object on its left. It is also worth noting that the standard library usually requires that the types stored in the container have assignment operators, and their return value is a reference to the operation object on the left.

The assignment operator should usually return a reference to the operation on its left side.

Synthetic copy assignment operator

Like processing copy constructors, if a class does not define its own copy assignment operator, the compiler will generate a synthetic copy assignment operator for it. Similar to copy constructors, for some classes, the synthetic copy assignment operator is used to prohibit assignment of objects of that type. If the copy assignment operator is not for this purpose, it will assign each non-static member of the right-hand operation object to the corresponding member of the left-hand operation object, which is done by the copy assignment operator of the member type. For members of an array type, assign array elements one by one. The synthetic copy assignment operator returns a reference to its left-hand operation object.

As an example, the following code is equivalent to the synthetic copy assignment operator of Sales_data:

//Equivalent to synthetic reference bene assignment operatorSales_data&
Sales_data::operator=(const Sales_data&rhs)
{
    bookkNo=//Call string::operator=    units_sold=rhs.units_sold;//Use built-in int assignment    revenue=;//Use built-in double assignment    return*this;//Return a reference to this object}

Destructor

The destructor performs the opposite operation to the constructor: the constructor initializes the non-static data member of the object, and may do some other work; the destructor releases the resources used by the object and destroys the non-static data member of the object.

A destructor is a member function of a class, and its name is composed of tildes and class names. It has no return value and does not accept parameters

class Foo{
public:
~Foo();//Destructor}

Since the destructor does not accept parameters, it cannot be overloaded. For a given class, there will be only a unique destructor.

What does the destructor do

Just as a constructor has an initialization part and a function body, the destructor also has a function body and a destructor. In a constructor, the initialization of members is done before the function body is executed and is initialized in the order in which they appear in the class. In a destructor, the function body is first executed and the members are destroyed. Members are destroyed in reverse order in the initialization order. After the object is last used, the body of the destructor performs any final work the class designer wishes to perform. Typically, the destructor releases all resources allocated by the object during its lifetime. In a destructor, there is no such thing as initializing a list in the constructor to control how members are destroyed, and the destructor part is implicit. What happens when a member is destroyed depends entirely on the type of member. Destroying members of a class type requires executing the members' own destructor. Built-in types have no destructors, so destroying built-in type members requires nothing to do.

Implicitly destroying a member of a built-in pointer type will not delete the object it points to.

Unlike ordinary pointers, smart pointers are class types, so they have destructors. Therefore, unlike ordinary pointers, smart pointer members are automatically destroyed during the destruction stage. When will the destructor be called

Whenever an object is destroyed, its destructor will be automatically called:

  • Variables are destroyed when they leave their scope.
  • When an object is destroyed, its members are destroyed.
  • When a container (whether it is a standard library container or an array) is destroyed, its elements are destroyed.
  • For dynamically allocated objects, they are destroyed when the delete operator is applied to the pointer to it.
  • For temporary objects, they are destroyed when the full expression of creating it ends.

Since the destructor runs automatically, our programs can allocate resources as needed without (usually) worrying about when to release them.

For example, the following code snippet defines four sales_data objects:

{//New scope//p and p2 point to dynamically allocated objectsSales_data *p=new Sales_data;//p is a built-in pointerauto p2=make_shared<Sales_data>();//p2 is a shared_ptrSales_data item(*p);//Refers to the Bei constructor to copy *p into the itemvector<Sales_data>vec;//Local objectvec.push_back(*p2);//Copy the object pointed to by p2delete p;//Execute destructor on the object pointed to by p
}//Exit local scope; call destructors for item, p2 and vec//Destroying p2 will reduce its reference count; if the reference count becomes 0, the object is released//destroyvecIts elements

Each Sales_data object contains a string member, which allocates dynamic memory to save characters in bookNo members. However, the only memory that our code needs to directly manage is the Sales_data object we allocate directly. Our code simply releases the dynamic allocated object bound to p.

Other Sales_data objects will be automatically destroyed when they leave scope. When the program block ends, vec, p2 and item are all out of scope, meaning that destructors of vector, shared_ptr and Sales_data will be executed on these objects, respectively. The vector's destructor destroys the elements we add to vec. The destructor of shared_ptr decrements the reference count of the object pointed to by p2. In this example, the reference count becomes 0, so the destructor of shared_ptz deletes the Sales_data object allocated by p2.

In all cases, the Sales_data destructor implicitly destroys the bookNo member. Destruction of bookNo calls the string's destructor, which frees the memory used to save the ISBN.

The destructor is not executed when a reference or pointer to an object leaves the scope.

Synthetic destructor

When a class does not define its own destructor, the editor defines a synthetic destructor for it. Similar to copy constructors and copy assignment operators, for tree classes, synthetic destructors are used to prevent objects of this type from being destroyed. If this is not the case, the body of the synthesized destructor is empty.

For example, the following code snippet is equivalent to the synthetic destructor of Sales_data:

class Sales_data{
public:
    //Members will be automatically destroyed, and there is no need to do anything else other than that    ~Sales_data()《
    //Definition of other members, as before}

After the (empty) destructor body is executed, the member will be automatically destroyed. In particular, the destructor of string will be called, which will free up the memory used by bookNo members.

It is very important to realize that the destructor body itself does not directly destroy members. Members are destroyed in the destructor phase implicitly after the destructor body. During the entire object destruction process, the destructor body is performed as another part of the member destruction step.

Three/Five Rules

As mentioned earlier, the three basic operations on the right can control the copy operations of the class: copy constructor, copy assignment operator and destructor. Moreover, under the new standard, a class can also define a moving constructor and a moving assignment operator.

C++ does not require us to define all these operations: we can define only one or two of them, without having to define all of them. However, these operations should usually be considered as a whole. Usually, it is rare to only need one of the operations, without defining all operations.

Classes that require destructors also require copying and assignment operations

When we decide whether a class wants to define its own version of the copy control member, a basic principle is to first determine whether the evil class requires a destructor. Generally, the requirement for destructors is more obvious than the requirement for copy constructors or assignment operators. If this class requires a destructor, we are almost certain that it also requires a copy constructor and a copy assignment operator.

The HasPtr class we used in our exercises is a good example. This class allocates dynamic memory in the constructor. The synthetic destructor does not delete a pointer data member. Therefore, this class requires defining a destructor to free the memory allocated by the constructor.

What should be done may be a bit unclear, but the basic principles tell us that HasPtr also requires a copy constructor and a copy assignment operator.

If we define a destructor for HasPtr, but use the synthetic version of the copy constructor and copy assignment operator, consider what happens:

class HasPtr{
public:
HasPtr(const std:;string&s=std::string()):
ps(new std::string(s)),i(0){}
~HasPtr(){deleteps;
//Error: HasPtr requires a copy constructor and a copy assignment operator//Definition of other members, as before};

In this version of the class definition,The memory allocated in the constructor will beHasPtrReleased when the object is destroyed。But unfortunately,We introduced a serious mistake!This version of the class uses the synthetic copy constructor and copy assignment operator。These functions simply copy pointer members,This means multipleHasPtrObjects may point to the same memory:

HasPtr f(HasPtr hp)// HasPtr is a value-passing parameter, so it will be referred to as Be{
    HasPtr ret=hp;//Copy the given HasPt    //Processing ret    return ret;//Ret and hp are destroyed}

whenfWhen returning,hpandretAll were destroyed,Called on both objectsHasPtrdestructor。This destructor willdelete retandhpPointer member in。But these two objects contain the same pointer value。This code causes this pointer to bedeletetwice,This is obviously a mistake。What is going to happen is undefined。

also,fThe caller will also use thefObject of:

```cpp
HasPtr("some values");
f(p);//When the king ends, the meat storage pointed to by p is releasedHasPtr(p);//NowpandqAll point to invalid memory!

The memory pointed to by p and q is no longer valid and is returned to the system when hp (or ret!) is destroyed.

Classes that require copy operations also require assignment operations, and vice versa

Although many classes need to define all (or do not need to define any) copy control members, the work to be done by some classes only requires copy or assignment operations, and does not require destructors. As an example, consider a class assigning a unique, unique sequence number to each object. This class requires a copy constructor to generate a new and unique sequence number for each newly created object. Besides this, this copy constructor copies all other data members from the given object. This class also requires a custom copy assignment operator to invite

Do not assign serial numbers to the target object. However, this class does not require a custom destructor.

This example introduces a second basic principle: if a class requires a copy constructor, it is almost certain that it also needs a copy assignment operator. And vice versa - If a class requires a copy assignment operator, it is almost certain that it also requires a copy constructor. However, whether it is necessary to copy the constructor or copy the assignment operator does not necessarily mean that the destructor is also required.

Use =default

We can explicitly ask the compiler to generate a synthetic version by defining the copy control member as =default

class Sales_data{
public:
//Copy control member; use defaultSales_data()=default;
Sales_data(const Sales_data&)=default;
Sales_data& operator=(const Sales_data&);
~Sales_data()=default;
//Definition of other members, as beforeSales_data &Sales_data::operator=(const Sales_data&)=default;

When we modify the declaration of a member with =default within a class, the synthesized function will be implicitly declared as inline (just like any other member function declared within a class). If we do not want the synthesized members to be inline functions, we should only use =default for the off-class definitions of the members, just as we do with the copy assignment operator.

Block copy

Although most classes should define (and usually define) copy constructors and copy assignment operators, these operations have no reasonable meaning for some classes. In this case, some mechanism must be used to prevent copying or assignment when defining a class. For example, the iostream class blocks copying to avoid multiple objects writing or reading the same IO buffer. To prevent copying, it looks like the copy control member should not be defined. However, this strategy is invalid: if our class does not define these operations, the compiler generates a synthetic version for it.

Define deleted functions

Under the new standard, we can prevent copying by defining the copy constructor and copy assignment operator as deleted functions. The deleted functions are such a function: we cannot use them in any way, although we declare them. Add =delete after the function's parameter list to indicate that we want to define it as deleted:

struct NoCopy{
NoCopy()=default;//Use the default constructor of the synthesisNoCopy(const NoCopy&)=delete;//Block copyingNoCopy&operator=(const NoCopy&)=delete;//Block assignment~NoCopy() = default; // Use synthetic destructor// Other members

A type that removes the destructor, the compiler will not allow defining variables of that type or creating temporary objects of the class. Moreover, if a class has a member type that deletes the destructor, we cannot define variables or temporary objects of that class. Because if a member's destructor is deleted, the member cannot be destroyed. If a member cannot be destroyed, the object as a whole cannot be destroyed.

For types that deleted the destructor, although we cannot define variables or members of this type, objects of this type can be assigned dynamically. However, these objects cannot be released:

struct NoDtor{
NoDtor()=default;//Use the synthesis default constructor~NoDtor()=delete;//We cannot destroy objects of type NoDtorNoDtor nd;//Error: The destructor of NoDtor is just removedNoDtor*p=new NoDptor();//Correct: But we can't delete pdelete p;//Error: The destructor of NoDtor is culled}

The synthesized copy control member may be deleted

As mentioned earlier, if we do not define the copy control member, the compiler defines the synthesised version for us. Similarly, if a class does not define a constructor, the compiler will synthesize a default constructor for it. For some classes, the compiler defines these synthetic members as deleted functions:

  • If the destructor of a member of the class is deleted or inaccessible (eg, private), the synthetic destructor of the class is defined as deleted.
  • If the copy constructor of a member of the class is deleted or inaccessible, the synthetic copy constructor of the class is defined as deleted.
  • If the destructor of a member of the class is deleted or inaccessible, the copy constructor of the class composition is also defined as deleted.
  • If the copy assignment operator of a class tree member is deleted or inaccessible, or the class has a const or reference member, the class's synthetic copy assignment operator is defined as deleted.
  • If the destructor of a member of a class is deleted or inaccessible, or the class has a reference member, it does not have an in-class initializer, or the class has a const member, it does not have an in-class initializer, and its type does not explicitly define the default constructor, the default constructor of the class is defined as deleted.

Essentially, the meaning of these rules is: if a class has data members that cannot be constructed, copied, copied or destroyed by default, the corresponding member function will be defined as deleted.

It may seem strange that a member has a deleted or inaccessible destructor that causes the synthesis's default and copy constructor to be defined as deleted. The reason is that without this rule, we may create objects that cannot be destroyed.

For classes with reference members or const members that cannot be constructed by default, it should not be surprising that the creator does not synthesize the default constructor for it. The same unsurprising rule is that if a class has a const member, it cannot use the synthetic copy assignment operator. After all, this operator tries to assign all members, and it is impossible to assign a new value to a const object.

Although we can assign a new value to a reference member, what is changed is the reference to the value of the object pointed to, rather than the reference itself. If a copy assignment operator is synthesized for such a class, after assignment, the left-hand operation object still points to the same object as before assignment, and will not point to the same object as the right-hand operation object. Since this behavior does not seem to be what we expect, for classes with reference members, the synthetic copy assignment operator is defined as deleted.

Essentially, when it is impossible to copy, assign or destroy a member of a class, the class's synthetic copy control member is defined as deleted.

Private copy control

Before the release of the new standard, classes blocked copy by declaring their copy constructors and copy assignment operators as private:

class PrivateCopy{
//No access specifier; the next member defaults to private;//The copy control member is private, so the code of ordinary user cannot be accessedPrivateCopy(const PrivateCopy&);
PrivateCopy&operator=(const PrivateCopy&);
//Other memberspublic:
PrivateCopy()=default;//Use the default constructor of the synthesis~PrivateCopy();//The user can define objects of this type, but cannot copy them}

Since the destructor is public, users can define objects of type PrivateCopy. However, since the copy constructor and copy assignment operator are private, the user code will not be able to copy objects of this type. However, friend and member functions can still copy objects. To prevent copying of friends and member functions, we declare these copy control members as private, but do not define them.

It is legal to declare but not define a member function, with only one exception. Attempting to access an undefined member will result in a link-time error. By declaring (but not defining) private copy constructors, we can pre-block any attempt to copy an object of that type: the user code trying to copy the object will be marked as an error during the compilation phase; copy operations in member functions or friend functions will result in link time errors.

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.