13.6 对象移动
有时候对象拷贝之后就立即销毁了,如果只是移动而不是拷贝对象会大幅度提升性能,因为移动不用重新分配内存和拷贝数据
- 标准库容器、
string
和shared_ptr
类既支持移动也支持拷贝,IO
类和unique_ptr
类可以移动但不能拷贝
13.6.1 右值引用
右值引用只能绑定到一个将要销毁的对象,可以自由地将一个右值引用的资源移动到另一个对象中
对于常规的左值引用,不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用则相反,但不能直接绑定到一个左值上
int i = 10;
int &r1 = i; // OK, 左值引用
int &&r2 = i; // Err, 不能将右值引用绑定到左值
int &r3 = i * 10; // Err, i*10是一个右值
const int &r3 = i * 10; // OK, 可以将const引用绑定到右值
int &&r4 = i * 10; // OK, 右值引用
cout << r4 << endl; // 100
r4 += 10; // OK,也可以修改
- 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是左值
- 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值
int& func1(int& num) {
num = num * num;
return num;
}
int func2(int num) {
return num * num;
}
int main(int argc, char const *argv[]) {
int a = 5;
int &ref1 = func1(a); // 返回左值
cout << ref1 << endl; // 25
int b = 4;
int &&ref2 = func2(b); // 返回右值
cout << ref2 << endl; // 16
const int &ref3 = func2(b); // const引用可以绑定右值
cout << ref3 << endl; // 16
return 0;
}
右值引用指向将要被销毁的对象,所以,我们可以从绑定到右值引用的对象“窃取”状态
变量是左值
int &&r1 = 10;
int &&r2 = r1; // Err, r1是左值,尽管r1是右值引用,但还是一个变量
std::move()
可以显式地将一个左值转换为对应的右值引用类型
可以用std::move
函数来获得绑定到左值上的右值引用
int &&r1 = 10;
int &&r2 = std::move(r1); // OK
调用std::move()
之后,除了对r1
赋值或销毁外,不能再使用它
13.6.2 移动构造函数和移动赋值运算符
移动构造函数
移动构造函数的参数是右值引用,新对象接管源对象的资源之后,把源对象的指针置空,使其成为可析构的状态
- 必须把源对象指针置空,不然源对象移动完就析构,移动的资源也跟着释放了
class Person
{
private:
string *ptr;
int age;
public:
Person(const string &s = string()) : ptr(new string(s)), age(0) {}
Person(const Person &p) : ptr(new string(*p.ptr)), age(p.age) {}
Person(Person &&p) noexcept // 移动操作不应抛出异常
: ptr(p.ptr), age(p.age) {
cout << "move constructor" << endl;
p.ptr = nullptr; // 源对象的指针置空
}
~Person() {
delete ptr;
}
};
int main(int argc, char const *argv[]) {
Person p1;
Person p2 = std::move(p1);
return 0;
}
move constructor
移动赋值运算符
先释放自身的资源,接管源对象的资源,再把源对象的指针置空
class Person {
private:
string *ptr;
int age;
public:
Person& operator=(Person &&rhs) noexcept {
cout << "move copy assignment" << endl;
if (this != &rhs) {
delete ptr; // 释放拥有的资源
ptr = rhs.ptr;
age = rhs.age;
rhs.ptr = nullptr;
}
return *this;
}
};
Person func(Person& p) { // 函数返回右值
return p;
}
int main(int argc, char const *argv[]) {
Person p1, p2;
p2 = func(p1);
return 0;
}
move copy assignment
如果只定义拷贝构造函数,没有定义移动构造函数,那也可以用右值进行构造,调用的是拷贝构造函数
拷贝并交换赋值运算符和移动操作
可以将拷贝赋值运算符和移动赋值运算符统一起来,调用自定义
swap
函数即可
class Person
{
private:
string *ptr;
int age;
public:
Person(const string &s = string()) : ptr(new string(s)), age(0) {}
Person(const Person &p) : ptr(new string(*p.ptr)), age(p.age) {
cout << "copy constructor" << endl;
} // 拷贝构造函数
Person(Person &&p) noexcept // 移动构造函数
: ptr(p.ptr), age(p.age)
{
cout << "move constructor" << endl;
p.ptr = nullptr; // 源对象的指针置空
}
Person& operator=(Person rhs) {
cout << "swap copy assignment" << endl;
swap(*this, rhs);
return *this;
}
friend void swap(Person&, Person&);
~Person() { delete ptr; }
};
inline
void swap(Person& lhs, Person& rhs) {
using std::swap;
swap(lhs.ptr, rhs.ptr);
swap(lhs.age, rhs.age);
}
int main(int argc, char const *argv[])
{
Person p1("kavin"); // 直接初始化
Person p2;
p2 = p1;
cout << "----------" << endl;
p2 = std::move(p1);
return 0;
}
// copy constructor 左值调用拷贝构造函数
// swap copy assignment
// ----------
// move constructor 右值调用移动构造函数
// swap copy assignment
在拷贝赋值运算符中调用自定义的swap
函数,注意参数是非引用的,所以,当实参是左值时,调用拷贝构造函数,当实参是右值时,调用移动构造函数
13.6.3 右值引用和成员函数
可以同时定义拷贝和移动版本的成员函数
void push_back(const string&);
void push_back(string&&);
当调用push_back
时,实参类型决定了新元素是拷贝还是移动
StrVec vec;
string s = "hello";
vec.push_back(s); // 调用push_back(const string&)
vec.push_back("done"); // 调用push_back(string&&)
右值和左值引用成员函数
可以给成员函数增加引用限定符,强制左侧运算对象是一个左值/右值
class Person
{
private:
string *ptr;
int age;
public:
Person& operator=(const Person&) &;
};
Person& Person::operator=(const Person& rhs) &
{
// 执行将rhs赋予本对象的工作
return *this;
}
如果同时使用
const
和引用限定,const
必须排在前面重载和引用函数
可以综合引用限定符和
const
来区分一个成员函数的重载版本
class Foo
{
private:
vector<int> data;
public:
Foo sorted() &&; // 可用于可改变的右值
Foo sorted() const &; // 可用于任何类型的Foo
};
// 本对象为右值,因此可以原址排序
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
return *this;
}
// 本对象是const或左值,不能对其进行原址排序
Foo Foo::sorted() const &
{
cout << "left value" << endl;
Foo ret(*this); // 先拷贝对象
sort(ret.data.begin(), ret.data.end());
return ret;
}
对象是一个右值,意味着没有其他用户,因此可以改变对象
当对一个const
右值或一个左值执行sorted
时,不能改变对象,因此需要在排序前拷贝对象
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符