【C++ primer阅读记录】移动构造函数和移动赋值函数
对象移动
新标准的一个最主要的特性就是可以移动而非拷贝对象的能力,在其中某些情况下,对象拷贝后就立即销毁了,这些情况下移动而非拷贝对象会大幅度提升性能。使用移动而不是拷贝的另一个原因在于源于IO类或Unique_ptr类,这些类都包含不能被共享的资源。(如指针或IO缓冲)。(这种拷贝一般是指类值拷贝?然而某些资源是不允许使用类值拷贝又另外一个副本对象共享资源的)。
新标准中,我们可以用容器保存不可拷贝的对象,只要他们能够移即可移动。
string和shared_ptr即支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
用移动来代替拷贝可以避免出现被拷贝对象的副本,出现两个用户的变量。
右值引用
新标准引入了一种新的引用类型---右值引用。左值与右值的根本区别在于能否获取内存地址。
右值引用的性质:只能绑定到一个将要销毁的对象。可以自由地将一个右值引用的资源"移动"到另一个对象中。
对于常规引用,我们可以称之为左值引用,不能绑定到要求转换的表达式、字面常量或返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表示式上。
int i=42;
int &r = i; //i具有内存地址,可以引用这个对象
const int &r3 = i*43; //i*43返回的是一个右值(或者在这里是一个常量?),所以可以使用常量引用
int &&rr2 = i*43; //将rr2绑定到乘法结果上,这个结果其实也会占用内存,但是很快就被释放了吧?
返回左值引用的函数,赋值、下标、解引用和递增递减运算符都是返回左值的表达式的例子。(这些例子返回的对象都是可以取得内存空间的)。
左值持久,右值短暂
左值有持久的状态(固定的内存空间),右值要么是字面常量、要么在表达式求值过程中创建临时对象。
右值引用:1)所引用的对象将要被销毁 2)该对象没有其他用户
因此右值引用的代码可以自由地接管所引用的对象的资源。
标准库move函数
右值引用不能直接绑定到左值上,但是可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。
move告诉编译器:我们有一个左值,但我们希望像一个右值一样处理他。调用move就意味着,除了对这个值赋值或销毁它外,将不在使用它。我们不能对移后源对象的值做任何假设。
使用move的代码应该使用std::move而不是move。、
移动构造函数
为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,但它们从给定的对象“窃取”而不是拷贝资源。
拷贝构造函数的第一个参数是该类型的一个引用,移动构造函数的第一个参数是右值引用,确保移后源对象处于销毁它是无害的状态。一旦资源完成移动,源对象必须不再指向被移动的资源,已经归属新创建的对象。
拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。移动构造函数首先将传递参数的内存地址空间接管。然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。
StrVec::StrVec(StrVec &&s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap)
{//noexcept是我们在构造函数中承诺函数不抛出异常的方法。
s.elements = s.first_free = s.cap = nullprt;
}
与拷贝构造函数不同,移动构造函数并不创建新的内存空间来存储要赋的值,它接管给定的StrVec中的内存。接管内存之后将给定对象中的指针都置为nullptr,就完成了从给定对象的移动操作。最终移后源对象会被销毁,意味着在其上运行析构函数。
由于移动操作不分配新资源,通常是不会抛出异常的。当编写一个不抛出异常的移动操作时,应该事先通知标准库。
移动赋值运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作。类似拷贝赋值函数,移动赋值运算符必须正确处理自赋值。
StrVec& StrVec::operator=(StrVec &&rhs) noexcept
{
if(this!=&rhs)
{
free(); //释放已有元素
elements = rhs.elements; //从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
//移后源对象进入一个可析构的状态,通过将移后源对象的指针成员置为nullptr来实现
}
return *this;
}
移后源约定
1.任何对象被移动后,如果该对象失效,可以用另一个有效对象向其赋值,使之重新变为有效对象。
2.任何对象被移动后,仍然可以被正常析构。
这只是开发者们的一种约定,从一个对象中移动数据并不会销毁此对象(也就是rhs仍然存在),但是在移动操作之后源对象有可能会被销毁,因此我们需要在移动之后,移后源对象进入一个可析构的状态,通过将移后源对象的指针成员置为nullptr来实现。
除了将移后源置为析构安全的状态之外,移动操作还必须保证对象仍是有效的。也就是上面的第一点。
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设
合成的移动操作
如果一个类定义了自己的拷贝构造函数、拷贝赋值函数或者析构函数,编译器就不会合成移动构造函数和移动赋值函数了。只有当一个类没有定义自己任何版本的拷贝控制成员,且类中的每个数据成员都可以移动的时候,编译器才会合成移动构造函数或移动赋值符。
调用移动构造函数
Class Foo
{
public:
Foo() = default;
Foo(const Foo&);
};
Foo x;
Foo y(x);
Foo z(std::move(x)); //拷贝构造函数
对z进行初始化的时候,调用了move(x),它返回一个绑定到x的Foo&&,Foo的拷贝构造函数是可行的,会将Foo&&转换成const Foo&。
值得注意的是,用拷贝构造函数代替移动构造函数几乎是安全的。
同时在赋值运算符中调用移动或拷贝操作
class HasPtr
{
public:
HasPtr(HasPtr &&p) noexpetr: ps(p.ps),i(p.1){p.ps = 0;}
//添加了移动构造函数,此函数不会抛出异常,将其标记为noexcept。
HasPtr& operator=(HasPtr rhs)
{
swap(*this,rhs);return *this;
}
}
赋值运算符接受一个非引用参数,那么传形参的时候就会进行拷贝初始化,因此拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数。如果传入的是左值,使用拷贝构造函数,传入的是右值,使用的是移动构造函数。从而单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能。
用一个单一参数为右值引用的赋值函数可以兼容两种构造方式。
移动迭代器
在StrVecc的reallocate成员中使用for寻源调用construct从旧内存中拷贝到新内存中。作为一种替换方法,调用uninitialized_copy来构造新分配的内存也可以。但是也是进行了拷贝操作。
新标准库中定义了一种移动迭代器适配器,一个移动迭代器通过改变给定的迭代器的解引用符行为来适配此迭代器。(迭代器的适配器!)。移动迭代器的解引用生成一个右值引用。
我们通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器,此函数接受一个迭代器参数,返回一个移动迭代器。
void StrVec::reallocate()
{
auto newcap = size()?2*size():1;
auto first = alloc.allocate(newcap);
auto last = unitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first); //本来应该是begin()和end()解引用的数传入到新空间中构造元素。
free();//释放旧空间,对旧空间执行destroy+deallocate。
elements = first;
first_free = last;
cap = elements + newcap;
}
uninitialized_copy对输入序列中的每个元素调用construct拷贝到目的位置上,此算法使用迭代器的解引用运算符从输入序列中提取元素。construct将用移动构造函数来构造元素。
由于移动一个对象可能销毁原对象,因此只有在确信算法使用完该对象不再访问它时,才将移动迭代器传递给算法。

浙公网安备 33010602011771号