c++对象移动

标准库类、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

一、右值引用

必须绑定到右值的引用,通过&&来获得右值引用。
只能绑定到一个将要销毁的对象,因此我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。
右值引用则相反,可以绑定到这类表达式上,但不能讲一个右值引用直接绑定到一个左值上。

1
2
3
4
5
6
int i = 42;
int &r = i;
int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; //错误:i*42是一个右值
const int &r3 = i * 42;
int &&rr2 = i * 42;

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值。

返回非引用类型的函数,连同算数、关系、位以及后置递增/递减运算符,都生成右值。可以讲一个const的左值引用或者一个右值引用绑定到这类表达式上。

左值持久、右值短暂

左值有持久的状态,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象,我们得知

所引用的对象将要被销毁
该对象没有其他用户

使用右值引用的代码可以自由地接管所引用的对象的资源。

变量是左值

把看成变量表达式,变量表达式都是左值。

1
2
int &&rr1 = 42;
int &&rr2 = rr1; //错误:表达式rr1是左值

标准库move函数

通过move获得绑定到左值上的右值引用。头文件utility。

1
int &&rr3 = std::move(rr1);	//正确

move告诉编译器:我们有一个左值,但希望像右值一样处理它。调用move就意味着承诺:除了对rr1赋值或销毁它外,将不再使用它。

二、移动构造函数和移动赋值运算符

从给定对象“窃取”资源。
移动构造函数第一个参数是该类型的一个右值引用。与拷贝函数一样,任何额外的参数都必须有默认实参。一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源的所有权已经归属新创建的秀爱哪个。

1
2
3
4
5
StrVec::StrVec(StrVec &&s)noexcept
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}

noexcept通知标准库构造函数不抛出任何异常。

移动赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}

移动后源对象必须可析构,例如指针成员设置为nullptr。

合成的移动操作

只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。

1
2
3
4
5
6
7
8
9
10
11
struct X{
int i;
std::string s;
};

struct hasX{
X mem;
};

X x,x2 = std::move(x); //使用合成的移动构造函数
hasX hx,hx2 = std::move(hx); //使用合成的移动构造函数

移动操作永远不会隐式定义为删除的。

1
2
3
4
5
6
7
struct hasY
{
hasY() = default;
hasY(hasY &&) = default;
Y mem; //hasY将有一个删除的移动构造函数
};
hasY hy, hy2 = std::move(hy); //错误:移动的构造函数是删除的

如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。反之亦然。

移动右值,拷贝左值

1
2
3
4
5
6
7
StrVec v1, v2;		

v1 = v2; //v2是左值;使用拷贝赋值

StrVec getVec(istream &);

v2 = getVec(cin); //getVec(cin)是一个右值;使用移动赋值

赋予v2的是getVec调用的结果。此表达式是一个右值。此情况下,两种赋值运算符都是可行的。调用拷贝复制运算符需要进行一次到const的转换,而StrVec&&则精准匹配。因此使用移动赋值运算符。

如果没有移动构造函数,右值也被拷贝

1
2
3
4
5
6
7
8
9
10
class Foo{
public:
Foo() = default;
Foo(const Foo&);
//未定义移动构造函数,则默认为删除的
};

Foo x;
Foo y(x); //拷贝构造函数;x是一个左值
Foo z(std::move(x)); //拷贝构造函数,因为未定义移动构造函数

move(x)返回一个绑定到x的Foo&&。Foo的拷贝构造函数是可行的,将一个Foo&&转换为一个const Foo&,因此z的初始化将使用Foo的拷贝构造函数。

如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”
的。

拷贝并交换赋值运算符和移动操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; //rhs是临时变量,函数结束后,会销毁
}
}

hp = hp2; //使用拷贝构造函数来初始化rhs
hp = std::move(hp2);//使用移动构造函数,通过std::move(hp2)来初始化rhs

更新三/五法则

所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

三、右值引用和成员函数

定义了push_back的标准库容器提供两个版本:一个版本有一个右值引用参数,另一个版本有一个const左值引用。

1
2
void push_back(const X&);	//拷贝:绑定到任意类型的X
void push_back(X&&); //移动:只能绑定到类型X的可修改的右值

第二个版本对于非const的右值是精确匹配的,因此当我们传递一个可修改的右值时,编译器会选择运行这个版本。

区分移动和拷贝的重载函数通常有一个版本接受const T&,另一个版本接受T&&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class StrVec
{
public:
void push_back(const std::string&);
void push_back(std::string&&);
};

void StrVec::push_back(const std::string& s)
{
chk_n_alloc();
alloc.construct(first_free++,s);
}

void StrVec::push_back(td::string &&s)
{
chk_n_alloc();
alloc.construct(first_free++,std::move(s));
}

当我们调用push_back时,实参类型决定了新元素是拷贝还是移动。

1
2
3
4
5
6
7
StrVec vec;

string s = "some string";

vec.push_back(s); //调用push_back(const string&)

vec.push_back("done"); //调用push_back(string &&)

引用限定符(&、&&)

可以使&或&&,分别指出this可以指向一个左值或右值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo{
public:
Foo &operator=(const Foo&)&; //只能向可修改的左值赋值
};

Foo &Foo::operator=(const Foo&rhs)&
{
return *this;
}

Foo &retFoo();
Foo retVal();

Foo i,j;
i = j;
retFoo() = j;
retVal() = j; //错误:retVal()返回一个右值
i = retVal();

class Foo
{
public:
Foo someMem() & const; //错误:const限定符必须在前
Foo someMem() const &; //正确:const限定符在前
};

重载和引用函数

引用限定符也可以区分重载版本。我们可以综合引用限定符合和const来区分一个成员函数的重载版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Foo
{
public:
Foo sorted() &&;
Foo sorted() const &;
private:
vector<int> data;
};

//本对象是右值,可以原址排序
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
return *this;
}

//本对象是const或是一个左值,哪种情况都不能进行原址排序
Foo Foo::sorted() const &&
{
Foo ret(*this); //拷贝一个副本
sort(ret.data.begin(), ret.data.end());//排序副本
return ret; //返回副本
}

retVal().sorted(); //retVal()是一个右值,调用Foo::sorted() &&
retFoo().sorted(); //retFoo()是一个左值,调用Foo::sorted() const &

当我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或所有都不加。

1
2
3
4
5
6
7
8
9
class Foo{
public:
Foo sorted() &&;
Foo sorted() const; //错误,必须加上引用限定符

using Comp = bool(const int&, const int&);
Foo sorted(Comp*);
Foo sorted(Comp*) const; //正确:两个版本都没有引用限定符
};

如果一个成员函数有引用限定符,则具有相同参数列表的所有版本必须有引用限定符。