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 const
reference), 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-
static
member 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
- When using
- 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
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
operator
keyword followed by an operator symbol - If an operator is a member function, its left-hand object is bound to the implicit
this
parameter. 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- static
data 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
delete
the object it points to, and the smart pointer is automatically destroyed during the destructor phaseDestructor 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
delete
For 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
delete
will 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
=default
explicitly 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
,=delete
it 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
private
prevent 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_ptr
manage the resources in the class. If you want to manage resources directly, you can use reference count (reference count) , and then redefine HasPtr
it 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.
0
Destroys 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 swap
function 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
swap
The function is not necessary, but it is an important optimization method- Defined
swap
classes oftenswap
define 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,
string
andshared_ptr
types support both move and copy.IO
Classes andunique_ptr
classes 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
move
functions can obtain rvalue references bound to lvaluesint &&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,
noexcept
it informs the standard library that this function will not throw an exception. Must be specified in both declaration and definitionnoexcept
If marked, the
noexcept
move constructor will be used, otherwise the copy constructor will be used.Such operations may require reallocation
vector
of 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_back
vector
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
static
The 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 functionA 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_iterator
which 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 definitionX &
. Because move constructors need to steal data and usually pass an rvalue reference, the actual parameter cannot beconst
. And the operation of the copy constructor should not change the object, so the normalX &
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 const
a member function, by specifying a reference qualifier after the parameter list, the specified this
left 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 itthis
can point to an lvalue&&
indicatesthis
a pointer to an rvalue- The reference qualifier and
const
can exist at the same time,const
before 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- static
data 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