C++ Study Notes (13) - Copy Control

C++ Study Notes (13) - Copy Control

This article will learn how classes control object copying, assignment, moving, and destruction through a set of functions: copy constructor, move constructor, copy assignment operator, move assignment operator, and destructor. If the class does not explicitly define these copy control members, the compiler will automatically define them.

Copy, Assign and Destroy

copy constructor

If the first parameter of a constructor is a reference to its own class type (almost always a constreference), and any additional parameters have default values, then it is a copy constructor, which is used implicitly, so it should not beexplicit

Synthetic copy constructor

If no copy constructor is defined for the class, the compiler will define it, and even if other constructors are defined, the compiler will synthesize the copy constructor

  • Synthetic copy constructor: Copies each non- staticmember in turn from the given object into the object being created
  • For class types, use its copy constructor to copy; for built-in types, copy directly; for arrays, copy element by element

copy initialization

  • Direct initialization: requires the compiler to use ordinary function matching to select the best matching constructor, including copy constructors
  • Copy initialization: requires the compiler to copy the right-hand operand into the object being created, and perform type conversion if necessary
  • Copy initialization is usually done with copy constructors, and sometimes with move constructors
  • Copy initialization happens
    • When using =define variables
    • Passing an object as an argument to a formal parameter of a non-reference type
    • Returns an object from a function whose return type is not a reference type
    • Initialize an element of an array or a member of an aggregate class with a braced list
  • The parameter of the copy constructor must be a reference type, because in order to call the copy constructor, the actual parameters must be copied, and the copy constructor will be called again, an infinite loop
class Sales_data{
    public:
        Sales_data(const Sales_data& orig): bookNo(orig.bookNo), units_sold(orig.units_sold)) {}  // 拷贝构造函数,第一个参数为引用,且通常为const
    private:
        std::string bookNo;
        int units_sold = 0;
}
string dots(10, '.');  // 直接初始化
string s(dosts);  // 直接初始化,因为是调用最匹配的构造函数,包括拷贝构造函数
string s2 = dots;  // 拷贝初始化

refer to

A big misunderstanding of C++ - in-depth explanation of the difference between direct initialization and copy initialization

copy assignment operator

Similar to the copy constructor, if the class does not define its own copy assignment operator, the compiler generates a synthetic copy assignment operator

  • Overloaded operators are essentially functions whose names consist of a operatorkeyword followed by an operator symbol
  • If an operator is a member function, its left-hand object is bound to the implicit thisparameter. If it is a binary operator, its right-hand operand is passed as the display parameter
  • An assignment operator should normally return a reference to its left-hand operand

destructor

The destructor releases the resources used by the object and destroys the non- staticdata members of the object

  • The destructor is a member function, the name consists of a tilde, has no return value, accepts no parameters, cannot be overloaded, and is unique in a class

    ~Foo();  // 析构函数
  • In the destructor, the function body is executed first, then the members are destroyed, destroyed in the reverse order of the initialization order, and all resources allocated by the object during its lifetime are released

  • Implicitly destroys a member of a built-in pointer type without deletethe object it points to, and the smart pointer is automatically destroyed during the destructor phase

  • Destructor call time (when the object is destroyed)

    • 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
    • deleteFor a dynamically allocated object, it is destroyed when an operator is applied to the pointer to it
    • For a temporary object, it is destroyed when the full expression that created it ends
  • The destructor does not execute when a reference or pointer to an object goes out of scope

  • The destructor body itself does not directly destroy the members, the members are destroyed in the implicit destruction phase after the destructor body

The rule of three/five

  • If a class needs a custom destructor, it almost certainly needs a custom copy assignment operator and copy constructor (such as simply copying a pointer member, causing multiple class objects to point to the same memory, which deletewill go wrong)
  • If a class needs a copy constructor, it almost certainly needs a copy assignment operator too, and vice versa. But that doesn't mean it needs a page destructor

use=default

  • By defining a copy-controlled member as =defaultexplicitly asking the compiler to generate a synthetic version, it can only be used on member functions that have a synthetic version, i.e. default constructors or copy-controlled members
  • When a modified member is declared in a class `=default, it is implicitly expressed as inline. If you do not want the synthetic member to be an inline function, you can define it outside the class and use it

prevent copying

When defining a class, you can take the function of defining delete to prevent copying or assignment, because for some classes, these operations may not make sense

  • The new standard defines that copying can be prevented by defining the copy constructor and copy assignment operator as deleted functions , i.e. adding a function parameter list after the=delete
  • Unlike =default, =deleteit must appear when the function is first declared
  • Unlike =default, can be specified for any function=delete
  • The destructor cannot be deleted, otherwise the object cannot be destroyed. For a type with its destructor removed, the compiler is not allowed to define variables of that type or create temporary objects of the class, but objects of these types can be dynamically allocated but not freed
  • Before the new standard, copy constructors and copy assignment operators could be declared to privateprevent copying, but it was not recommended

Copy Control and Resource Management

The copy operation can separate the behavior of the type into two

  • It looks like a value: that is, it has its own state. After copying, the copy and the meta object are completely independent, and changing the copy will not affect the original object, and vice versa
  • Looks like a pointer: it shares state, and when copying an object of this type, the copy uses the same underlying data as the original. Changing the copy changes the original and vice versa

A class that behaves like a value

  • For pointer members, you should have your own copy, otherwise it will point to the same underlying data as the pointer in the copied object
class HasPtr{
    public:
        // 构造函数都动态分配自己的 string 副本,并将指向该 string 的指针保存到 ps 中
        HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
        HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
        HasPtr& operator=(const HasPtr &);
        ~HasPtr() { delete ps; }  // 对 ps 执行 delete, 释放分配的内存
    private:
        std::string *ps;
        int i;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps);  // 拷贝底层 string
    delete ps;  // 释放旧内存
    ps = newp;  // 从右侧运算对象拷贝数据到本对象
    i = rhs.i;
    return *this;  // 返回本对象
}
int main(void)
{
    HasPtr hasPtr1("hao");  // 直接初始化,第一个构造函数
    HasPtr hasPtr2(hasPtr1); // 直接初始化,调用拷贝构造函数
    HasPtr hasPtr4;
    hasPtr4 = hasPtr1;  // 拷贝赋值运算符
}
  • When writing assignment operators, pay attention to two points
    • The assignment operator must work correctly if an object is assigned to itself
    • Most assignment operators combine the work of destructors and copy constructors

Define a class that behaves like a pointer

The best way to make a class exhibit pointer-like behavior is to shared_ptrmanage the resources in the class. If you want to manage resources directly, you can use reference count (reference count) , and then redefine HasPtrit to use reference count instead ofshared_ptr

How reference counting works

  • The constructor (except the copy constructor) initializes the object, creates a reference count, records the number of shared state objects, and initializes the counter to 1
  • The copy constructor does not allocate a new counter, but copies the data members of the given object, including the counter, and the counter is incremented
  • The destructor decrements the counter, if the counter becomes 0, the destructor releases the state
  • The copy assignment operator increments the counter of the right-hand operand and decrements the counter of the left-hand operand. 0Destroys if the left operand counter is
class HasPtr{
    public:
        // 拷贝构造函数分配新的 string 和新的计数器, 将计数器置为 1
        HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
        // 拷贝构造函数拷贝所有三个数据成员,并递增计数器
        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;  // 用来记录有多少个对象共享 *ps 的成员
};
HasPtr::~HasPtr()
{
    if (--*use == 0){  // 如果引用计数变为 0
        delete ps;     // 释放 string 内存
        delete use;    // 释放计数器内存
    }
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    ++*rhs.use;  // 递增右侧运算对象的引用计数
    if(--*use == 0)  // 然后递减本对象的引用计数
    {
        delete ps;   // 如果没有其他用户,则释放本对象分配的成员
        delete use;
    }
    ps = rhs.ps;  // 将数据从 rhs 拷贝到本对象
    i = rhs.i;
    use = rhs.use;
    return *this;  // 返回本对象
}

swap operation

Tears that manage resources also define a swapfunction called , for those algorithms that remake the order of elements, which is called when elements are swappedswap

  • Swapping objects requires one copy and two assignments
  • If the class is defined swap, the algorithm will use the custom version of the class, otherwise, the algorithm will use the standard library-defined versionswap
  • swapThe function is not necessary, but it is an important optimization method
  • Defined swapclasses often swapdefine their assignment operator, using a technique called copy-and-swap, which swaps the left-hand operand with a copy of the right-hand operand
  • Assignment operators using copy and swap are automatically exception-safe and handle self-assignment correctly
class HasPtr{
    friend void swap(HasPtr&, HasPtr&);  // 定义为 friend 可访问私有成员
    // ...
};
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;  // 若存在类型特定的 swap 版本,匹配程度会优于 std 定义版本
    swap(lhs.ps, rhs.ps);  // 交换指针,而不是 string 数据
    swap(lhs.i, rhs.i);  // 交换 int 成员
}

// 参数是按值传递,故调用拷贝构造函数创建 rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{   // 交换左侧运算对象和局部变量 rhs 的内容
    swap(*this, rhs);  // rhs 现在指向本对象曾经使用的内存
    return *this;  // rhs 被销毁,从而 delete 了 rhs 中的指针
}

object movement

The new standard defines the characteristics of moving objects, which greatly improves performance over copying objects

  • Standard library containers, stringand shared_ptrtypes support both move and copy. IOClasses and unique_ptrclasses can be moved but not copied

rvalue reference

An rvalue reference is a reference that must be bound to an rvalue

  • An rvalue reference can only be bound to one and will be destroyed, so state can be stolen from an object bound to an rvalue reference

  • An lvalue expression represents the identity of an object, while an rvalue expression represents the value of the object

  • A regular reference is an lvalue reference and cannot be bound to an expression requiring conversion, a literal constant, or an expression that returns an rvalue

  • rvalue references have the exact opposite binding properties, you cannot bind an rvalue reference to an lvalue
int i = 42;
int &r = i;  // 正确:r 引用 i
int &&rr = i;  // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42;  // 错误:i * 42 是右值
const int &r3 = i *42;  // 正确:可以将 const 引用绑定到右值上
int &&rr2 = i * 42;  // 正确:将 rr2 绑定到乘法结果上
  • Variables are lvalues, so an rvalue reference cannot be directly bound to a variable, even if the variable is an rvalue reference type.

  • Standard library movefunctions can obtain rvalue references bound to lvalues

    int &&rr1 = 42;  //  字面值常量是右值
    int &&rr3 = std:;move(rr1);  // ok

Move constructor and move assignment operator

In order for a custom type to support move operations, you need to define a move constructor and move assignment operator for it

  • The first parameter of the move constructor is an rvalue reference, and any additional parameters must have default arguments

  • After the resource is moved, the source object must no longer point to the moved resource, and the ownership of these resources has been assigned to the newly created object

  • Move operations usually do not allocate any resources, so usually no exceptions are thrown. The new standard definition specifies that after the function parameter list, noexceptit informs the standard library that this function will not throw an exception. Must be specified in both declaration and definitionnoexcept

  • If marked, the noexceptmove constructor will be used, otherwise the copy constructor will be used.

    Such operations may require reallocation vectorof memory space. Using a move constructor and throwing an exception after moving some elements can cause problems because the moved source element has been changed. And if the copy constructor is used, the requirements are metpush_backvector

  • Unlike copy operations, the compiler does not synthesize move operations for some classes

    • Move constructors and move assignment operators are not synthesized if the class defines a copy constructor, copy assignment operator, or destructor
    • staticThe compiler will only synthesize a move constructor or move assignment operator if the class does not define any copy control members of its own version, and every non-data member of the class is moveable
  • Unlike copy operations, move operations are never implicitly defined as functions for deletion

  • If we show a move operation that requires the compiler to generate =default, and the compiler cannot move all members, the compiler will define the move operation as a deleted function

  • A class that defines a move constructor or move assignment operator must also define its own copy operation; otherwise, the class's synthetic copy constructor and copy assignment operator are defined as deleted

  • If a class has a copy constructor available but no move constructor, its objects are moved through the copy constructor. The copy assignment operator is similar to the move assignment operator

class HasPtr{
    public:
        HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; }
        HasPtr& operator=(HasPtr rhs) { swap(*this, rhs); return *this; }
    // ... 同上
}
int main()
{
    HasPtr hp, hp2;
    hp = hp2;  // hp2 是左值; hp2 通过拷贝构造函数来拷贝
    hp = std::move(hp2);  // 移动构造函数移动 hp2
}
  • Move iterator : Dereference generates an rvalue reference, make_move_iteratorwhich converts an ordinary iterator to a move iterator through a standard library function

    // 使用移动迭代器,原对象可能被销毁
    unitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first);

rvalue references and member functions

Overloaded functions that distinguish between move and copy usually have one version that accepts one const T&and the other that accepts oneT &&

const X&&A version that accepts one or an ordinary argument is usually not required for a function definition X &. Because move constructors need to steal data and usually pass an rvalue reference, the actual parameter cannot be const. And the operation of the copy constructor should not change the object, so the normal X &parameter version is not needed

// 定义了 push_back 的标准库容器提供了两个版本
void push_back(const X&);  // 拷贝:绑定到任意类型的 X
void push_back(X&&);  // 移动:只能绑定到类型 X 的可修改的左值

string s = "hao"
vector<string> vs;
vs.push_back(s);  // 调用 push_back(const string&);
vs.push_back("happy");  // 调用 push_back(string &&); 精确匹配

reference qualifier

Similar to defining consta member function, by specifying a reference qualifier after the parameter list, the specified thisleft and right/rvalue attributes can only be used in (non static) member functions, and must appear in both the function's declaration and semantics

  • &indicates that it thiscan point to an lvalue
  • &&indicates thisa pointer to an rvalue
  • The reference qualifier and constcan exist at the same time, constbefore the reference qualifier
  • Reference qualifiers also distinguish overloads
// 旧标准中会出现向右值赋值的情况
string s1 = "a value", s2 = "another";
s1 + s2 = "wow!"; 
// 新标准可通过引用限定符解决上述问题
class Foo{
    public:
        Foo &operator=(const Foo&) &;  // 只能像可修改的左值赋值
        // ... Foo 的其他参数
        Foo someMem() & const;  // 错误:const限定符必须在前
        Foo anotherMem() const &;  // 正确
        Foo sorted() &&;  // 用于可改变的右值,可以原址排序
        Foor sorted() const &;  // 对象为const 或左值,两种情况都不能进行原址排序
};
Foo &Foo::operator=(const Foo &rhs) & 
{   
    // 其它工作
    return *this;
}

Epilogue

Each class controls the copy, move, assignment, and destruction operations of objects of that type through the copy constructor, move constructor, copy assignment operator, move assignment operator, and destructor. The move constructor and move assignment operator accept a (usually NOT const) rvalue reference, while the copy version accepts a (usually const) plain lvalue reference

If the class does not declare these operations, the compiler will automatically generate them. If these operations are not defined to delete, the object is initialized, moved, assigned, or destroyed on a per-member basis: the composite operation processes each non- staticdata member in turn, determining how to move, copy, assign, and destroy it based on the member

Classes that allocate memory or other resources almost always need to define copy control members to manage the allocated resources, and if a class needs destructors, it almost certainly needs to define move and copy constructors and move and copy assignment operators as well

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325628728&siteId=291194637