2021-09-05 learning record of C++ Primer: Chapter 13

Chapter 13 copy control

13.1 copy, assignment and destruction

Five copy control operations:

  • copy constructor
  • copy assignment operator
  • move constructor
  • Move assignment operator
  • Destructor
13.1.1 copy constructor

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

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

The parameters of a copy constructor are almost always a reference to const and should not normally be explicit.

(1) Composite constructor

If we do not define a copy constructor for a class, the compiler will define one for us.

Even if we define other constructors, the compiler will synthesize a copy constructor for us.

  • Class type: use copy constructor to copy
  • Built in type: direct copy
  • Array: copies the members of an array type element by element

(2) Copy initialization

Difference between copy initialization and direct initialization:

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 using direct initialization, we actually ask the compiler to use ordinary function matching to select the constructor that best matches the parameters we provide.

When we use copy initialization, we require the compiler to copy the right operand to the object being created, and perform type conversion if necessary.

Copy initialization occurs not only when we define variables with '=', but also in the following cases:

  • Pass an object as an argument to a formal parameter of a non reference type
  • Returns an object from a function whose return type is a non reference type
  • Initializes an element in an array or a member in an aggregate class with a curly brace list

(3) Parameters and return values

In the process of function call, parameters with non reference type shall be copied and initialized. Similarly, when a function has a non referenced return type, the return value will be used to initialize the caller's result.

The copy constructor is used to initialize non reference class type parameters, which explains why the copy constructor's own parameters must be reference types. If its parameter is not a reference type, the call will never succeed - in order to call the copy constructor, we must copy its arguments, but in order to copy the arguments, we need to call the copy constructor, such an infinite loop.

(4) The compiler can bypass the copy constructor

During copy initialization, the compiler can (but does not have to) skip the copy / move constructor and create the object directly. That is, the compiler is allowed to convert the following code:

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

Rewrite to:

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

However, even if the compiler bypasses the copy / move constructor, the copy / move constructor must exist and accessible at this program point (for example, it cannot be private).

13.1.2 copy assignment operator

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

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

An assignment operator should normally return a reference to the operand to its left.

(1) Composite copy assignment operator

As with the copy constructor, if a class does not define its own copy assignment operator, the compiler generates a composite copy assignment operator for it.

Its copy principle is almost the same as that of the copy constructor.

13.1.3 destructor

(1) What does the destructor do

In a destructor, the function body is executed first, and then the members are destroyed. Members are destroyed in reverse order of initialization.

After the object is last used, the function body of the destructor can perform any finishing work that the class designer wants to do. Typically, destructors release all resources allocated by an object during its lifetime.

In a destructor, there is nothing like the initialization list in the constructor to control how members are destroyed. The destructor part is implicit. What happens when a member is destroyed depends entirely on the type of member. Destroying a member of a class type requires the member's own destructor to be executed. There is no destructor for built-in types, so nothing needs to be done to destroy built-in type members.

(2) When will the destructor be called

When a reference or pointer to an object leaves the scope, the destructor does not execute.

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

  • A variable is destroyed when it leaves its scope.
  • When an object is destroyed, its members are destroyed.
  • When a container (whether a standard library container or an array) is destroyed, its elements are destroyed.
  • A dynamically allocated object is destroyed when the delete operator is applied to the pointer to it.
  • For a temporary object, it is destroyed when the complete expression that created it ends.

(3) Synthetic destructor

When a class does not define its own destructor, the compiler defines a composite destructor for it. Similar to copy constructor and copy assignment operator, for some classes, composite destructors are used to prevent objects of this type from being destroyed. If this is not the case, the function body of the composite destructor is empty.

The destructor body itself does not directly destroy members. Members are destroyed during the implicit deconstruction phase after the destructor body. In the whole object destruction process, the destructor body is carried out as another part other than the member destruction step.

13.1.4 three / five rule
  • Classes that require destructors also require copy and assignment operations
  • Classes that require copy operations also require assignment operations, and vice versa
13.1.5 use = default

We can explicitly require the compiler to generate a composite version by defining the copy control member as = default:

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

When we modify the declaration of a member with = Default in the class, the composite function will be implicitly declared inline (just like any other member function declared in the class). If we don't want the synthesized member to be an inline function, we should only use = default for the definition outside the class of the member.

We can only use = default (that is, the default constructor or copy control member) for member functions with a composite version. Most classes should define default constructors, copy constructors, and copy assignment operators, whether implicit or explicit.

13.1.6 prevent copying

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

struct NoCopy 
{
    NoCopy() = default;                          // Use the default constructor of the composite;
    NoCopy(const NoCopy&) = delete;              // Block copy
    NoCopy &operator=(const NoCopys) = delete;   // Block assignment
    ~NoCopy() = default;                         // Using synthetic destructors
    // Other members
};

Unlike = default, = delete must appear when the function is first declared, and we can specify = delete for any function (we can only use = default for the default constructor or copy control member that the compiler can synthesize).

(1) Destructors cannot be deleted members

For types with destructors removed, although we cannot define variables or members of this type, we can dynamically allocate objects of this type. However, you cannot release these objects:

struct NoDtor 
{
    NoDtor() = default;      // Use composite default constructor
    ~NoDtor() = delete;      // We cannot destroy objects of type NoDtor
};
NoDtor nd;                   // Error: the destructor of NoDtor was deleted
NoDtor *p = new NoDtor();    // Correct: but we can't delete p 
delete p;                    // Error: the destructor of NoDtor was deleted

(2) A composite copy control member may be deleted

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.

  • Destructor of a member = delete or inaccessible = = > default constructor, composite destructor, composite copy constructor = delete
  • Copy constructor of a member = delete or inaccessible = = > composite copy constructor = delete
  • Copy assignment operator of a member = delete or inaccessible = = > composite copy assignment operator = delete
  • With const member or reference member and no in class initializer = = > default constructor = delete

(3) private copy control

Before the release of the new standard, a class prevents copying by declaring its copy constructor and copy assignment operator private:

class PrivateCopy 
{
    // No access specifier; The following members are private by default;
    // The copy control member is private, so ordinary user code cannot access it 
    PrivateCopy(const PrivateCopy&);
    PrivateCopy &operator=(const PrivateCopys);
    // Other members 
public:
    PrivateCopy() = default;   // Use the default constructor of the composite
    ~PrivateCopy();            // Users can define objects of this type, but they cannot copy them;
};

13.2 copy control and resource management

13.2.1 class of behavior image value
class HasPtr 
{
public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) { }
    // For the string pointed to by ps, each HasPtr object has its own copy 
    HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) { } 
    HasPtr& operator=(const HasPtr&);
    ~HasPtr() { delete ps; }
private:
    std::string *ps; 
    int i;
};

(1) Class value copy assignment operator

Assignment operators usually combine the operations of destructors and constructors. There are two general considerations:

  • The ability to assign an object to itself
  • Most assignment operators combine the work of destructors and copy constructors
// For version 1, the order of release and copy shall be considered
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps);  // Copy underlying string
    delete ps;                        // Free old memory
    ps = newp;                        // Copy data from the right operand to this object
    i = rhs.i;
    return *this;                     // Return this object
}
// The second version does not need to consider the order of release and copy
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    if (this == &rhs)
        return *this;
    delete ps;                        // Free old memory
    auto newp = new string(*rhs.ps);  // Copy underlying string
    ps = newp;                        // Copy data from the right operand to this object
    i = rhs.i;
    return *this;                     // Return this object
}
13.2.2 define classes that behave like pointers
class HasPtr 
{ 
public:
    // The constructor assigns a new string and a new counter, setting the counter to 1 
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new std::size_t(1)) { }
    // The copy constructor copies all three data members and increments the counter 
    HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) { ++*use; } 
    HasPtr& operator=(const HasPtr&);
    ~HasPtr(); 
private:
    std::string *ps; 
    int i;
    std::size_t *use;   // It is used to record how many objects share * ps members;
};

HasPtr::~HasPtr ()
{
    if (--*use == 0)    // If the reference count becomes 0
    { 
        delete ps;      // Free string memory 
        delete use;     // Free counter memory
    }
}

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    ++*rhs.use;         // Increments the reference count of the operand on the right 
    if(--*use == 0)     // Then decrements the reference count of this object
    {
        delete ps;      // If there are no other users 
        delete use;     // Release members assigned by this object
    }
    ps = rhs.ps;        // Copy data from rhs to this object
    i = rhs.i; 
    use = rhs.use;
    return *this;       // Return this object
}

13.3 switching operation

In addition to defining copy control members, classes that manage resources usually define a function called swap. For classes that are used with algorithms that reorder elements, it is important to define swap. Such algorithms call swap when two elements need to be exchanged.

If a class defines its own swap, the algorithm will use the custom version of the class. Otherwise, the algorithm uses the swap defined by the standard library. For the above HasPtr class, we prefer swap to exchange pointers rather than allocate a new copy of the string. That is, we want to exchange two hasptrs in this way:

class HasPtr 
{
    friend void swap(HasPtr&,HasPtrs);
    // Other member definitions, as in section 13.2.1
}; 

inline
void swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps);     // Swap pointers instead of string data 
    swap(lhs.i, rhs.i);        // Swap int members
}

Each swap call should be unqualified. That is, every call should be swap, not std::swap. If there is a type specific version of swap, it will match better than the version defined in std. Therefore, if there is a type specific version of swap, the swap call will match it. If there is no type specific version, the version in std is used (assuming that there is a using declaration in the scope).

(1) Use swap in assignment operators

Classes that define swap often use swap to define their assignment operators. These operators use a technique called copy and exchange. This technique exchanges a copy of the left operand with a copy of the right operand:

// Note that rhs is passed by value, meaning the copy constructor of HasPtr
// Copy the string in the operand on the right to rhs 
HasPtr& HasPtr::operator=(HasPtr rhs)
{
    //Swap the contents of the left operand and the local variable rhs
    swap(*this, rhs);   // rhs now points to the memory used by this object
    return *this;       // The rhs is destroyed so that the pointer in the rhs is delete d
}

In this version of the assignment operator, the parameter is not a reference. We passed the right operand to the assignment operator by value.

13.4 copy control example (*)

13.5 dynamic memory management class (*)

13.6 object movement

13.6.1 right value reference

An R-value reference can only be bound to one object to be destroyed. It can be temporary objects such as literal constants, expressions that require conversion, or expressions that return r-values. These objects have two properties:

  • The referenced object will be destroyed
  • There are no other users for this object
int i = 42; 
int &r = i;               // Correct: r refers to i 
int &&rr = i;             // Error: cannot bind an R-value reference to an l-value
int &r2 = i * 42;         // Error: i * 42 is an R-value 
const int &r3 = i * 42;   // Correct: we can bind a const reference to an R-value
int &&rr2 = i * 42;       // Correct: bind rr2 to the multiplication result 

(1) Standard library move function

Although an R-value reference cannot be directly bound to an l-value, we can obtain the R-value reference bound to the l-value by calling a new standard library function called move, which is defined in the header file < utility >.

int &rr3 = std::move(rr1);    // ok

The move call tells the compiler that we have an lvalue, but we want to handle it like an lvalue. We must realize that calling move means a promise that we will not use rr1 except to assign it or destroy it. After calling move, we cannot make any assumptions about the value of the moved source object.

Code that uses move should use std::move instead of move. This avoids potential name conflicts.

13.6.2 move constructor and move assignment operator

The first argument to the move constructor is a reference to the class type. This reference parameter is an R-value reference in the move constructor. Any additional parameters must have default arguments.

In addition to completing the resource move, the move constructor must also ensure that the source object is in such a state after the move that it is harmless to destroy it. In particular, once the resource is moved, the source object must no longer point to the moved resource -- the ownership of these resources has belonged to the newly created object.

As an example, we define a move constructor for the StrVec class to move rather than copy elements from one StrVec to another:

StrVec::StrVec(StrVec &&s) noexcept // The move operation should not throw any exceptions
    //The member initializer takes over the resources in s
    : elements(s.elements), first_free(s.first_free), cap(s.cap)
{
    // It is safe to put s into a state where it is safe to run a destructor 
    s.elements = s.first_free = s.cap = nullptr;   
}

Unlike the copy constructor, the move constructor does not allocate any new memory; It takes over the memory in a given StrVec. After taking over memory, it sets the pointers in the given object to nullptr. This completes the move operation from the given object and the object will continue to exist. Finally, the source object will be destroyed after the move, which means that the destructor will run on it. StrVec destructor at first_ deallocate is called on free. If we forget to change s.first_free, the memory we just moved will be released by destroying the moved source object.

(1) Move operations, standard library containers, and exceptions

Since the move operation "steals" resources, it usually does not allocate any resources. Therefore, the move operation usually does not throw any exceptions. When writing a move operation that does not throw an exception, we should notify the standard library of this. Unless the standard library knows that our move constructor will not throw an exception, it will think that it may throw an exception when moving our class object, and do some extra work to deal with this possibility.

Move operations usually do not throw exceptions, but throwing exceptions is also allowed.

(2) Move assignment operator

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
    //Direct detection self assignment 
    if (this != &rhs)
    {
        free();                     // Release existing elements 
        elements = rhs.elements;    // Take over resources from rhs 
        first_free = rhs.first_free;
        cap = rhs.cap;
        // Placing rhs in a destructable state
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

Moving data from an object does not destroy the object, but sometimes the source object is destroyed after the move operation is completed. When we write a move operation, we must ensure that the source object enters a destructable state after the move. The move operation of our StrVec meets this requirement by setting the pointer member of the moved source object to nullptr.

The move operation must also ensure that the object is still valid. Generally speaking, an object valid means that it can be safely assigned a new value or can be safely used without relying on its current value.

On the other hand, the move operation has no requirements for the values left in the source object after the move. Therefore, the program should not rely on the data in the moved source object.

(3) Composite move operation

Only when a class does not define any copy control members of its own version (copy constructor, copy assignment operator, destructor), and all its data members can move construction or move assignment, the compiler will synthesize the move constructor or move assignment operator for it.

A class that defines a move constructor or move assignment operator must also define its own copy operation. Otherwise, these members are defined as deleted by default.

(4) There is no move constructor, and the right value is also copied

If a class has a copy constructor but no move constructor is defined, the compiler will not synthesize the move constructor, which means that the class will have a copy constructor but no move constructor. If a class does not have a move constructor, the function matching rule ensures that objects of this type will be copied, even when we try to move them by calling move:

class Foo 
{ 
public:
    Foo() = default;
    Foo(const Foo&);   // copy constructor 
    // Other members are defined, but Foo does not define a move constructor
}; 
Foo x; 
Foo y(x);              // Copy constructor; x is an lvalue
Foo z(std::move(x));   // Copy constructor because no move constructor is defined
13.6.3 right value reference and member function

In addition to constructors and assignment operators, a member function can also benefit if it provides both copy and move versions. This move enabled member function usually uses the same parameter pattern as the copy / move constructor and assignment operator - one version accepts an lvalue reference to const and the second version accepts an lvalue reference to non const.

For example, the standard library container that defines push back provides two versions: one has an R-value reference parameter and the other has a const l-value reference. Assuming that X is an element type, these containers define the following two push_back version:

void push_back(const X&);    // Copy: bind to any type of X
void push_back(X&&);         // Move: can only be bound to modifiable right values of type X

We can pass any object that can be converted to type X to the first version of push_back. This version copies data from its parameters. For the second version, we can only pass it non const r-values. This version is an exact match (and better match) for non const r-values, so when we pass a modifiable R-value, the compiler will choose to run this version. This version steals data from its parameters.

Generally speaking, we do not need to accept a const X & & or a (normal) x & parameter version for the function operation definition.

(1) R-value and l-value reference member functions

Usually, we call member functions on an object, regardless of whether the object is an lvalue or an lvalue. For example:

string s1 = "a value", s2 = "another"; 
auto n = (sl + s2).find('a');

In this example, we call the find member on a string right value, which is obtained by connecting two strings. Sometimes, the use of R-values can be surprising:

s1 + s2 = "wow! ";

Here, we assign a right value to the connection result of two string s.
In the old standard, there is no way to stop this use. In order to maintain backward compatibility, the new standard library class still allows assignment to the right value. However, we may want to block this usage in our own classes. In this case, we want to force the left operand (that is, the object pointed to by this) to be an lvalue.

We point out that the lvalue / rvalue attribute of this is defined in the same way as const member functions, that is, a reference qualifier is placed after the parameter list:

class Foo 
{
public:
    Foo &operator = (const Foo&) &; // Only modifiable lvalues can be assigned
    // Other parameters of Foo
};

Foo &Foo::operator = (const Foo &rhs) &&
{
    // Perform the work required to assign rhs to this object 
    return *this;
}

The reference qualifier can be & or & &, indicating that this can point to an lvalue or lvalue, respectively. Like const qualifiers, reference qualifiers can only be used for (non static) member functions and must appear in both the declaration and definition of the function.

For & qualified functions, we can only use it for lvalues; For & & qualified functions, only the right value can be used:

Foo &retFoo();     // Returns a reference; The retFoo call is an lvalue
Foo retVal();      // Returns a value; The retVal call is an rvalue
Foo i, j;          // i and j are lvalues 
i = j;             // Correct: i is an lvalue
retFoo() = j;      // Correct: retFoo() returns an lvalue 
retVal() = j;      // Error: retVal() returns an R-value
i = retVal();      // Correct: we can take an R-value as the right operand of the assignment operation

A function can be qualified with const and reference at the same time. In this case, the reference qualifier must follow the const qualifier.

(2) Overloaded and referenced functions

Reference qualifiers can also distinguish overloaded versions. We can combine the reference qualifier and const to distinguish the overloaded version of a member function.

For example, we will define a vector member named data and a member function named sorted for Foo. Sorted returns a copy of the Foo object, where the vector has been sorted:

class Foo 
{
public:
    Foo sorted() &&;            // Can be used for changeable right values 
    Foo sorted() const &;       // Can be used with any type of Foo
    // Definition of other members of Foo 
private:
    vector<int> data;
};

// This object is an R-value, so it can be sorted in the original address 
Foo Foo::sorted() &&
{
    sort(data.begin(), data.end()); 
    return *this;
}

// This object is const or an lvalue. In either case, we cannot sort its original address 
Foo Foo::sorted() const & 
{
    Foo ret(*this);                             // Copy a copy
    sort(ret.data.begin(), ret.data.end());     // Sort copy
    return ret;                                 // Return copy
}

When we sort an R-value, it can safely sort the data members directly. The object is an R-value, which means that there are no other users, so we can change the object. When sorting a const right value or an L value, we cannot change the object, so we need to copy the data before sorting.

The compiler will determine which sorted version to use according to the lvalue / rvalue attribute of the object calling sorted:

retVal().sorted();    // retVal() is an R-value. Call foo:: sorted()&& 
retFoo().sorted();    // retFoo() is an lvalue. Call foo:: sorted() const&

Tags: C++ C#

Posted on Fri, 17 Sep 2021 00:13:25 -0400 by NoobPHP