Chapter.13 Copy Constructor

1、The Copy Constructor

What is a copy constructor? When is it used?

A copy constructor is a constructor which first parameter is a reference to the class type and any additional parameters have

default values.

When copy initialization happens and that copy initialization requires either the copy constructor or the move constructor

  • Define variables using an =
  • Pass an object as an argument to a parameter of non-reference type
  • Return an object from a function that has a non-reference return type
  • Brace initialize the elements in an array or the members of an aggregate class
  • Some class types also use copy initialization for the objects they allocate.
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) // 1
{
    Point local = arg, *heap = new Point(global); // 2, 3
    *heap = local;
    Point pa[ 4 ] = { local, *heap }; // 4, 5
    return *heap; // 6
}


2、The Copy-Assignment Operator

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

The copy-assignment operator is function named operator=.This operator is used when assignment occurred.The synthesized copy-assignment operator assigns each non-static member of the right-hand object to corresponding member of the left-hand object using the copy-assignment operator for the type of that member.It is synthesized when the class does not define its own

3、The Destructor

What is a destructor? What does the synthesized destructor do? When is a destructor synthesized?

The destructor is a member function with the name of the class prefixed by a tilde(~).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. Otherwise, the synthesized destructor has an empty function body.

The compiler defines a synthesized destructor for any class that does not define its own destructor.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 orderfrom the order in which they were initialized

      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 typenote: The destructor is not run when a reference or a pointer to an object goes out of scope.

{
 // 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


#include <iostream>
#include <vector>
#include <initializer_list>

struct X {
    X() { std::cout << "X()" << std::endl; }                    //1
    X(const X&) { std::cout << "X(const X&)" << std::endl; }    //2
    X& operator=(const X&)                                      //3
    {
        std::cout << "X& operator=(const X&)" << std::endl;
        return *this;
    }
    ~X() { std::cout << "~X()" << std::endl; }                  //4
};

void f(const X& rx, X x)    // 2
{
    std::vector<X> vec;     //omit skip the constructor
    vec.reserve(2);         //omit skip the constructor

    X y = x;  //2
    y=rx;     //3

    vec.push_back(rx);        //2
    vec.push_back(x);         //2
}// destroy y, vec(twice), x  //4 times


int main()
{
    X* px = new X;   //1
    f(*px, *px);
    delete px;       //4
    return 0;
}


 The Rule of Three/Five

Classes That Need Destructors Need Copy and Assignment

Classes That Need Copy Need Assignment, and Vice Versa


Preventing Copies

The Destructor Should Not be a Deleted Member

reason: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

1、The synthesized destructor is defined as deleted if the class has amember whose own destructor is deleted or is inaccessible (e.g., private).
2、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.

3、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.

4、The synthesized default constructoris 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


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




Swap 

string *temp = v1.ps; // make a temporary copy of the pointer in v1.ps
v1.ps = v2.ps;        // assign the pointer in v2.ps to v1.ps
v2.ps = temp;         // assign the saved pointer in v1.ps to v2.ps

instead

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

swap Functions Should Call swap, Not std::swap

void swap(Foo &lhs, Foo &rhs)
{
     using std::swap;
     swap(lhs.h, rhs.h); // uses the HasPtr version of swap
     // swap other members of type Foo
}
Using swap in Assignment Operators 

Assignment operators that use copy and swap are automatically exception safe and correctly handle self-assignment

#include <string>
#include <iostream>

class HasPtr 
{
public:
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator<(const HasPtr &lhs, const HasPtr &rhs);

    HasPtr(const std::string &s = std::string()) 
        : ps(new std::string(s)), i(0) 
    { }

    HasPtr(const HasPtr &hp) 
        : ps(new std::string(*hp.ps)), i(hp.i) 
    { }

    HasPtr& operator=(HasPtr tmp) 
    {
        this->swap(tmp);
        return *this;
    }

    ~HasPtr() 
    {
        delete ps;
    }

    void swap(HasPtr &rhs) 
    {
        using std::swap;
        swap(ps, rhs.ps);
        swap(i, rhs.i);
        std::cout << "call swap(HasPtr &rhs)" << std::endl;
    }

    void show() const
    { 
        std::cout << *ps << std::endl; 
    }
private:
    std::string *ps;
    int i;
};

void swap(HasPtr& lhs, HasPtr& rhs)
{
    lhs.swap(rhs);
}

bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
    return *lhs.ps < *rhs.ps;
}

Essentially, the specific avoiding memory allocation is the reason why it improve performance. As for the pointerlike version, no dynamic memory allocation anyway. Thus, a specific version for the valuelike will not improve the performance.

Swap 

string *temp = v1.ps; // make a temporary copy of the pointer in v1.ps
v1.ps = v2.ps;        // assign the pointer in v2.ps to v1.ps
v2.ps = temp;         // assign the saved pointer in v1.ps to v2.ps

instead

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

swap Functions Should Call swap, Not std::swap

void swap(Foo &lhs, Foo &rhs)
{
     using std::swap;
     swap(lhs.h, rhs.h); // uses the HasPtr version of swap
     // swap other members of type Foo
}

Using swap in Assignment Operators

Assignment operators that use copy and swap are automatically exception safe and correctly handle self-assignment

#include <string>
#include <iostream>

class HasPtr 
{
public:
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator<(const HasPtr &lhs, const HasPtr &rhs);

    HasPtr(const std::string &s = std::string()) 
        : ps(new std::string(s)), i(0) 
    { }

    HasPtr(const HasPtr &hp) 
        : ps(new std::string(*hp.ps)), i(hp.i) 
    { }

    HasPtr& operator=(HasPtr tmp) 
    {
        this->swap(tmp);
        return *this;
    }

    ~HasPtr() 
    {
        delete ps;
    }

    void swap(HasPtr &rhs) 
    {
        using std::swap;
        swap(ps, rhs.ps);
        swap(i, rhs.i);
        std::cout << "call swap(HasPtr &rhs)" << std::endl;
    }

    void show() const
    { 
        std::cout << *ps << std::endl; 
    }
private:
    std::string *ps;
    int i;
};

void swap(HasPtr& lhs, HasPtr& rhs)
{
    lhs.swap(rhs);
}

bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
    return *lhs.ps < *rhs.ps;
}

Essentially, the specific avoiding memory allocation is the reason why it improve performance. As for the pointerlike version, no dynamic memory allocation anyway. Thus, a specific version for it will not improve the performance.




 Move Constructor and Move Assignment

concept:
Like the copy constructor, the move constructor has an initial parameter that is a reference to the class type. Differently from the copy constructor, the reference parameter in the move constructor is an rvalue reference. As in the copy constructor, any additional parameters must all have default arguments

As an example, we’ll define the StrVec move constructor:
StrVec::StrVec(StrVec &&s) noexcept // move won't throw any
exceptions
// member initializers take over the resources in s
: elements(s.elements), first_free(s.first_free),
cap(s.cap)
{
// leave s in a state in which it is safe to run the destructor
s.elements = s.first_free = s.cap = nullptr;
}

After an object is moved from, that object continues to exist. Eventually, the moved-from object will be destroyed, meaning that the destructor will be run on that object. The StrVec destructor calls deallocate on first_free. If we neglected to change s.first_free, then destroying the moved-from object would delete the memory we just moved.


exceptions for move:Because a move operation executes by “stealing” resources, it ordinarily does not itself
allocate any resources. As a result, move operations ordinarily will not throw any
exceptions. When we write a move operation that cannot throw, we should inform the
library of that fact. As we’ll see, unless the library knows that our move constructor
won’t throw, it will do extra work to cater to the possibliity that moving an object of
our class type might throw.

note:the move constructor must ensure that the moved-from object is left in a state such that destroying that object will be harmless
note:Move constructors and move assignment operators that cannot throw exceptions should be marked as noexcept.

    Understanding why noexcept is needed can help deepen our understanding of how
the library interacts with objects of the types we write. We need to indicate that a
move operation doesn’t throw because of two interrelated facts:
First, although move operations usually don’t throw exceptions, they are permitted to do so. 
Second, the library containers provide guarantees as to what they do if an exception happens



move-assignment operator
The move-assignment operator does the same work as the destructor and the move constructor. As with the move constructor, if our move-assignment operator won’t throw any exceptions, we should make it noexcept. Like a copy-assignment operator, a move-assignment operator must guard against self-assignment
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
// direct test for self-assignment
if (this != &rhs) {
    free(); // free existing elements
    elements = rhs.elements; // take over resources from rhs
    first_free = rhs.first_free;
    cap = rhs.cap;
    // leave rhs in a destructible state
    rhs.elements = rhs.first_free = rhs.cap = nullptr;
   }
   return *this;
}


Warning
After a move operation, the “moved-from” object must remain a valid, destructible object but users may make no assumptions about its value
1、setting the pointer members of the moved-from object to nullptr
2、a valid object is one that can safely be given a new value or used in other ways that do not depend on its current value



The Synthesized Move Operations

The compiler synthesizes the move constructor and move assignment only if
a class does not define any of its own copy-control members and only if all
the data members can be moved constructed and move assigned,
respectively.
// the compiler will synthesize the move operations for X and hasX
struct X {
    int i;         // built-in types can be moved
    std::string s; // string defines its own move operations
};
struct hasX {
     X mem; // X has synthesized move operations
};
     X x, x2 = std::move(x);        // uses the synthesized move constructor
     hasX hx, hx2 = std::move(hx); // uses the synthesized move constructor

note:Classes that define a move constructor or move-assignment operator must also define their own copy operations. Otherwise, those members are deleted by default.


Rvalues Are Moved, Lvalues Are Copied ...
If a class has a usable copy constructor and no move constructor, objects will be “moved” by the copy constructor. Similarly for the copy-assignment operator and move-assignment


Copy-and-Swap Assignment Operators and Move


class HasPtr {
public:
    HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { }
    // each HasPtr has its own copy of the string to which ps points
    HasPtr(const HasPtr &p): ps(new std::string(*p.ps)), i(p.i) { }
    /*
    HasPtr& operator=(const HasPtr &rhs)
    {
        auto newp = new string(*rhs.ps); // copy the underlying string
        delete ps;                       // free the old memory
        ps = newp;                       // copy data from rhs into this object
        i = rhs.i;
        return *this;                    // return this object
    }*/
    ~HasPtr() { delete ps; }
    
    
    // added move constructor
    HasPtr(HasPtr&& rhs) noexcept
    : ps(rhs.ps),i(rhs.i)
    {
        rhs.ps = 0;
    }
    // assignment operator is both the move-and copy-assignment operator
    HasPtr& operator=(HasPtr rhs)
    {
        swap(*this,rhs);
        return *this;
    }
private:
    std::string *ps;
    int i;
};


For example, assuming both hp and hp2 are HasPtr objects:
hp = hp2; // hp2 is an lvalue; copy constructor used to copy hp2
hp = std::move(hp2); // move constructor moves hp2
    Now let’s look at the assignment operator. That operator has a nonreference parameter, which means the parameter is copy initialized Depending on the type of the argument, copy initialization uses either the copy constructor or the move constructor; lvalues are copied and rvalues are moved. As a result, this single assignment operator acts as both the copy-assignment and move-assignment operator
   In the first assignment, the right-hand operand is an lvalue, so the move constructor is not viable. The copy constructor will be used to initialize rhs. The copy constructor will allocate a new string and copy the string to which hp2 points.
   In the second assignment, we invoke std::move to bind an rvalue reference to hp2. In this case,both the copy constructor and the move constructor are viable.However, because the argument is an rvalue reference, it is an exact match for the move constructor. The move constructor copies the pointer from hp2. It does not allocate any memory.
   Regardless of whether the copy or move constructor was used, the body of the assignment operator swaps the state of the two operands. Swapping a HasPtr exchanges the pointer (and int) members of the two objects. After the swap, rhs will hold a pointer to the string that had been owned by the left-hand side. That string will be destroyed when rhs goes out of scope.


Advice: Updating the Rule of Three
All five copy-control members should be thought of as a unit: Ordinarily, if a class defines any of these operations, it usually should define them all. As we’ve seen, some classes must define the copy constructor, copy-assignment operator, and destructor to work correctly . Such classes typically have a resource that the copy members must copy. Ordinarily, copying a resource entails some amount of overhead. Classes that define the move constructor and move-assignment operator can avoid this overhead in those circumstances where a copy isn’t necessary.






發佈了146 篇原創文章 · 獲贊 240 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章