第 13 章 拷贝控制
拷贝控制 copy control
操作包括:
拷贝构造函数 copy constructor
拷贝赋值运算符 copy-assignment operator
移动构造函数 move constructor
移动赋值运算符 move-assignment operator
析构函数 destructor
13.1 拷贝、赋值与销毁
合成拷贝构造函数 synthesized copy constructor
#include <iostream>
#include <string>
using namespace std;
class A {
public:
string name;
A() : name("") {
cout << "default ctor" << endl;
}
A(string name) : name(name) {
cout << "ctor" << endl;
}
A(const A& a) : name(a.name) {
cout << "copy ctor" << endl;
}
void print() {
cout << name << endl;
}
};
A func(A a) {
return a;
}
int main() {
cout << "a" << endl;
A a = A(); // default ctor
cout << "b" << endl;
A b; // default ctor
cout << "c" << endl;
A c("c"); // ctor
cout << "d" << endl;
A d = A("d"); // ctor
cout << "e" << endl;
A e(c); // copy ctor
cout << "f" << endl;
A f = c; // copy ctor
cout << "g = f(a)" << endl;
A g = func(a); // copy ctor; copy ctor
return 0;
}
关于 explicit 带来的变化
#include <iostream>
#include <string>
using namespace std;
class A {
public:
int id = 0;
A() {
cout << "default ctor" << endl;
}
A(int id) : id(id) {
cout << "ctor" << endl;
}
A(const A& a) : id(a.id) {
cout << "copy ctor" << endl;
}
void print() {
cout << id << endl;
}
};
A func(A a) {
return a;
}
class B {
public:
int id = 0;
B() {
cout << "default ctor" << endl;
}
explicit B(int id) : id(id) {
cout << "ctor" << endl;
}
B(const B& a) : id(a.id) {
cout << "copy ctor" << endl;
}
void print() {
cout << id << endl;
}
};
B funcb(B a) {
return a;
}
int main() {
A a(123); // ctor
A b = 123; // ctor
A c = func(a); // copy ctor; copy ctor
A d = func(123); // ctor; copy ctor
cout << "-------------------------" << endl;
B ba(123); // ctor
// B bb = 123; // compile error
B bc = funcb(ba); // copy ctor; copy ctor
// B bd = funcb(123); // compile error
return 0;
}
编译器可以绕过拷贝构造函数
编译器可以(但不是必需)跳过拷贝/移动构造函数直接创建对象。这也就是上面的程序为什么很多都没有实际打印出 copy ctor
。
例如
string s = "abc"; // 允许编译器改写为下面的代码
string s("abc");
虽然可以跳过,但是在这个程序点上,拷贝/移动构造函数必需是存在且可访问的。
13.1.2 拷贝赋值运算符
重载运算符 overloaded operator
。
重载运算符本质上是函数。
合成拷贝赋值运算符 synthesized copy-assignment operator
类似于拷贝构造函数他会将右侧每个非 static 成员赋予左边对象的对应成员。对于数组成员,会逐个赋值数组元素。
13.1.3 析构函数
构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和析构部分。
析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁。
什么时候会调用析构函数
变量离开作用域时被销毁。
当对象被销毁时其成员被销毁。
容器被销毁时,元素被销毁。
动态分配的对象,指向它的指针应用 delete 运算符时被销毁。
对临时对象,当创建它的完整表达式结束时被销毁。
13.1.5 使用 =default
我们可以通过将拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本。合成的函数将隐式的声明为内联的。如果不希望成员是内联函数,应该只对成员的类外定义使用 =default。
13.1.6 阻止拷贝
我们可以通过将这些可能被自动合成的函数定义为删除的函数 deleted function
来阻止拷贝。
struct NoCopy {
NoCopy(const NoCopy&) = delete;
NoCopy &operator=(const NoCopy&) = delete;
...
};
不同于 default 只能用于编译器能自动合成的函数,delete 关键字可以用于任何函数。有时需要利用 delete 引导编译器匹配正确的函数。
析构函数不能是删除的成员。删除析构函数本身并不会带来编译错误,但会导致不能创建对象(可以通过 new 动态分配,但不能 delete)。
合成的拷贝控制成员可能是删除的
如果类某个成员的析构函数是删除的或不可访问的(如 private 的),则合成析构函数被定义为删除的。
如果类的某个成员的拷贝构造函数/析构函数是删除的或不可访问的,则合成拷贝构造函数被定义为删除的。
如果类的某个成员的拷贝赋值运算符时删除的,或类有一个 const 的或引用成员,则类的拷贝赋值运算符被定义为删除的。
如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它某有类内初始化器,或是类有一个 const 成员,他没有类内初始化器且其类型未显式定义某人构造函数,则该类的默认构造函数被定义为删除的。
private 拷贝控制
新标准之前是靠把想删除的函数定义为 private 来实现拷贝控制的。
但注意这种方式,友元函数和成员函数依旧可以拷贝对象。
这里我们可以声明但不定义它们。这样我们如果尝试调用拷贝构造函数会导致一个链接时错误。
13.2 拷贝控制和资源管理
注意有的时候我们要让类的拷贝行为看起来像值,有的时候像指针,有的时候不能进行。
编写赋值运算符时应该检查:
一个对象赋值给自己时可以正常工作。
大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
如果要一个类的拷贝行为看起来像指针,则我们需要实现一套引用计数机制,这时我们需要一个引用计数器。
class LikePointer {
private:
std::size_t *ref_cnt;
public:
LikePointer() : ref_cnt(new std::size_t(1)) {}
LikePointer(const LikePointer &rhs) : ref_cnt(rhs.ref_cnt) {
++*ref_cnt;
}
LikePointer& operator=(const LikePointer &rhs) {
++*rhs.ref_cnt;
if (!--*ref_cnt) {
delete ref_cnt;
}
ref_cnt = rhs.ref_cnt;
return *this;
}
~LikePointer() {
if (!--*ref_cnt) {
delete ref_cnt;
...
}
}
};
13.3 交换操作
swap 的定义并不是必须的,但是对于分配了资源的类这可能会极大的提升效率。
class HasPtr {
std::string *ps;
int i;
friend void swap(HasPtr& lhs, HasPtr& rhs);
};
inline void HasPtr::swap(HasPtr& lhs, HasPtr& rhs) {
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
}
class Foo {
HasPtr h;
friend void swap(Foo &lhs, Foo &rhs);
};
void Foo::swap(Foo &lhs, Foo &rhs) {
using std::swap;
swap(lhs.h, rhs.h);
}
注意这里不要直接调用 std::swap。因为有些类型可能自己定义了 swap。
注意这里的 using 声明并不会隐藏掉 HasPtr 版本的 swap 的声明。
在赋值运算符中使用 swap
这里有一个计数称为拷贝并交换 copy and swap
HasPtr& HasPtr::operator=(HasPtr rhs) {
swap(*this, rhs);
return *this;
}
注意这里参数不是引用。
注意这里自动处理了自身赋值的情况。同样这个操作也是异常安全的,抛出异常只会发生在拷贝构造时,而这也是在修改左侧对象之前。
13.6 对象移动
13.6.1 右值引用
右值引用 rvalue reference
:必须绑定到右值(一个将要销毁的对象)。
我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
常规引用对应于右值称为左值引用 lvalue reference
。
int i = 42;
int &r = i;
// int &&rr = i; // 不能绑定到左值
// int &r2 = i * 42; // 不能绑定到右值
const int &r3 = i * 42; // 可以绑定到右值
int &&rr2 = i * 42;
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
变量是左值。不能将一个右值引用绑定到一个右值引用类型的变量上。
标准库 move 函数
我们可以通过 move 函数将一个左值转化为右值使用。但使用 move 后意味着我们除了对其赋值或销毁外,不能再使用它。
使用 move 的代码应当使用 std::move。
13.6.2 移动构造函数和移动赋值运算符
移动构造函数需要保证移动后源对象处在“销毁无害”的状态。
移动操作、标准库容器和异常
一般而言因为移动操作不需要分配资源因此不会抛出异常。
此时我们可以声明 noexcept 来通知标准库。
class A {
A(A&&) noexcept;
};
A::A(A&& rhs) noexcept {
...
}
例如 vector 如果在 push_back 时发生异常则 vector 自身不会变化。
但是对于移动构造函数,如果移动发生了一半,部分元素移动而不是全部元素移动后发生异常,旧空间的源元素已经被改变而新空间中未够早的元素尚不存在,则 vector 不能保证自身不变的要求。
所以如果 vector 不知道元素移动构造函数不会抛出异常时,会使用拷贝构造函数。
移动赋值运算符执行与析构函数和移动构造函数相同的工作。注意因为考虑 move 的情况,还是需要考虑自赋值的可能性。
合成的移动操作
如果一个类定义了自己的拷贝构造函数、拷贝赋值函数或析构函数,编译器就不会合成移动构造函数和移动赋值运算符了。
只有当一个类没有定义任何拷贝控制成员,且每个非 static 数据成员都可以移动时,才会合成移动构造函数或移动赋值运算符。
移动操作不会隐式定义为删除的函数。如果显式要求编译器生成 =default 的移动操作,且编译器不能移动所有成员,则编译器会奖移动操作定义为删除的函数。???
拷贝并交换赋值运算符和移动操作
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;
}
};
注意这个 operator= 可以同时实现拷贝赋值和移动赋值。
移动迭代器
我们可以用 make_move_iterator 函数将一个普通迭代器转换为一个移动迭代器。
13.6.3 右值引用和成员函数
允许成员函数使用和拷贝、移动构造函数相同的参数模式。
void push_back(const X&);
void push_back(X&&);
有时我们限定一些成员函数只能在左值上使用,这样我们可以使用引用限定符 reference qualifier
。
class Foo {
public:
Foo& operator=(const Foo&) &;
};
Foo &Foo::operator=(const Foo &rhs) & { ... }
注意限定符可以是 & 也可以是 && 分别表示限定为左值还是右值。应用限定符类似于 const 需要在声明和定义都包含。
重载和引用函数
可以同时使用 const 和引用限定,注意顺序
Foo func() const &;
我们可以重载不同引用限定符的版本。
class Foo {
public:
Foo sorted() &&;
Foo sorted() const &;
private:
vector<int> data;
};
Foo Foo::sorted() && {
sort(data.begin(), data.end());
return *this;
}
Foo Foo::sorted() const & {
Foo ret(*this);
sort(ret.data.begin(), ret.data.end());
return ret;
}
注意对于多个相同参数列表的重载函数,要么都使用引用限定符,要么都不使用。
Next: [模板][数据结构] 下标池 IdPool