右值引用
我们使用&&进行右值引用,而常规引用是使用&。
右值引用有一个重要特性就是只能绑定到一个将要销毁的对象。
左值引用不能将和要求转换的表达式、字面常量或是返回右值的表达式。
#include<iostream>
using namespace std;
int main(){
int i = 42;
int &r = i; //正确 引用i
//int &&rr = i; //i是左值,所以不能被右值引用rr所引用。
//int &r2 = i*42; //错误,i*42是一个右值。
int &&r2 = i*42;
const int &r3 = i*42;
}
- 赋值、下标、解引用和前置递增/递减运算符,都会产生左值。
- 算术、关系、位以及其后置递增/递减运算符都会产生右值。
标准库move函数
此函数定义在头文件utility,通过该函数可以显式地将一个左值转换到对应的右值引用。
int &&rr3 = std::move(rr1);
move告诉编译器:我们有一个左值,但是希望像一个右值去处理它。调用来move就意味着:除了对rr1进行赋值或销毁,将不再使用它
移动构造函数和移动赋值运算符
- 移动构造函数是从给定对象“窃取”资源而不是拷贝资源
- 移动构造函数的第一个参数是右值引用
- 移动构造函数要保证移动后的源对象处于 销毁 状态。
- 一旦资源完成移动,源对象不再指向这些资源,这些资源的所有权已经归属于新创建的对象
StrVec::StrVec(StrVec &&s) noexcept:
elements(s.elements), first_free(s.first_free), cap(s.cap){
//令s处于这样的状态——对其使析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
noexcept通知标准库
移动函数是窃取资源,不是拷贝资源。因此我们应该通知标准库,否则它会认为移动我们的类对象可能会抛出异常。
noexcept是我们承诺一个函数不抛出异常的方法。
class StrVec{
public:
StrVec(StrVec&&) noexcept;
};
StrVec::StrVec(StrVec &&s) noexcept:
{
//函数体
}
移动赋值运算函数
移动赋值运算函数也是要考虑移动后的源对象必须是可以析构的状态。
StrVec& StrVec::operator=(StrVec && rhs){
if(this != &rhs){
free();
elements = rhs.elements;
cap = rhs.cap;
first_free = rhs.first_free;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
合成的移动操作
如果不声明自己的拷贝构造函数或拷贝赋值函数,那么编译器就会生成合成的拷贝构造函数和拷贝赋值函数。
但是,对于移动操作,编译器对于某些类是不会合成移动操作的。如果一个类定义拷贝构造函数、拷贝赋值函数或析构函数,编译就不会为它合成移动操作。
只有一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static成员都可以移动时,编译器才会合成移动构造函数或移动赋值运算符。
编译器可以移动内置类型的成员和具有对应的移动操作的类
struct X{
int i;
string s;
};
struct hasx{
X mem;
};
X x, x2 = std::move(x);
hasx has1, hax2 = std::move(has1);
移动操作不会隐式定义为删除的函数。如果我们显式地要求编译器生成=default移动操作,但是编译器不能移动所有成员(就是有些成员不提供移动操作),那么编译器就会将移动操作定义为删除操作。
下面是移动操作被定义为删除操作的情况。
- 有类成员定义来拷贝构造函数,但是未定义移动构造函数;或者是有类成员没有定义拷贝构造函数,但是编译器不能为其合成移动构造函数。移动赋值运算符类似。
- 有类成员的移动构造函数或移动赋值运算符被定义为删除或者是不可访问的
- 析构函数被定义为删除的或不可访问的
- 有类成员是const或者是引用的,那么移动赋值运算符定义为删除的。
拷贝并交换赋值运算符和移动操作
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;}
我们为类添加了移动构造函数,构造函数体将给定的HasPtr的值设为0,从而确保销毁后移动源对象是安全的。
观察该赋值运算符,使用的是非引用参数,意味着该参数要进行拷贝初始化。
依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数——左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了拷贝赋值运算和移动赋值运算。
移动迭代器
StrVec的reallocate成员是使用for循环来调用construct从旧内存将元素拷贝到新内存中。
如果我们调用uninitialzed_copy来构造新的内存,同时有不执行拷贝。我们可以通过新标准定义的移动迭代器来完成。
//原始的reallocate
//reallocate实现重新分配内存,并构造元素
void StrVec::reallocate() {
auto newcapacity = size() ? 2 * size() : 1;
alloc_n_move(newcapacity);
}
void StrVec::alloc_n_move(size_t newcapacity) {
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i) {
alloc.construct(dest++, std::move(*elem++)); //move产生右值
}
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
通过移动迭代器make_move_iterator来实现。
void StrVec::reallocate(){
auto newcapacity = size() ? 2*size() : 1;
auto first = alloc.allocate(newcapacity);
auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()),first);
free();
elements = first;
first_free = last;
cap = elements + newcapacity;
}