从统一资源分配符讲解移动语义

前导知识:
1. 右值引用和其他引用一样,都是某个对象的别名
2. 右值引用绑定到的都是即将被销毁的并且没有其他用户的对象
3. 析构函数是系统进行析构前执行的最后一个函数,用以收尾,比如系统在析构某个指针的时候只会将栈上的指针析构掉而不会去把指针指向的那块内存析构掉,所以析构函数就是把这个指针指向的堆上的内存delete掉使其变成指向nullptr的空壳,然后系统进行析构时就不会造成内存泄漏了.理解析构函数做的工作并不是真正的析构,和系统进行的析构并不是同一个析构,这很重要.

为什么统一资源分配 ( unifying assignment operator )能work ?
几架马车都要有,想要统一资源分配符能跑的前提就是正确实现移动构造函数和swap函数
首先从名字看,”统一”指的是将移动赋值运算符和拷贝赋值运算符都统一至同一段代码中,这样做的好处显而易见,代码量更少,可读性更好
但关键是它为什么能够跑起来.
用《C++ Primer》 13.4节中的StrVec的做讲解,我们给它添加一个unifying assignment operator

//// 拷贝赋值运算符
//StrVec &StrVec::operator=(const StrVec &ori)
//{
//    std::cout<< "使用了拷贝赋值运算符" <<std::endl;
//    auto new_data{copy_n(ori.begin(), ori.end())};
//    free();
//    element = new_data.first;
//    first_free = cap = new_data.second;
//    return *this;
//}
//
//// 移动赋值运算符
//StrVec& StrVec::operator=(StrVec&& ori) noexcept
//{
//    std::cout<< "使用了移动赋值运算符" <<std::endl;
//    swap(*this, ori);
//    return *this;
//}

StrVec& StrVec::operator=(StrVec ori) noexcept
{
    // 使用统一资源分配
    std::cout<< "统一赋值运算符" <<std::endl;
    swap(*this, ori);
    return *this;
}

移动赋值运算符的形参是右值引用,而统一资源分配符传参是pass-by-value ,为什么要pass-by-value ?
我们首先看被统一的两个运算符的其中之一的拷贝赋值运算符做了什么工作
参数: const T&
行为: 和任何赋值操作一样,我们都希望在赋值操作完成之后,分配符左边的旧状态被销毁(比如将持有的资源释掉),那么我们就需要手动销毁左边的旧状态,
比如手动执行析构函数,然后分配新的内存,把赋值运算符右边的资源拷贝进新内存中,之后按需做一些其他的工作(比如StrVec中内存的三段标识符element,first_free,cap都要随着新内存的申请而更新至指向新内存),这样,就完成了状态的转义,this的旧状态被更新至新状态

接下来看移动赋值运算符,其实移动赋值运算符和拷贝赋值运算符是互补的,赋值运算符接受的参数是左值引用,移动赋值运算符接受的参数是右值引用
行为: 当右边的参数是纯粹的右值(表达式一结束该右值就被销毁了)或者是从左值std::move()得来的右值时,使用该移动赋值符实现移动语义.
其实它的代码写法和统一资源分配符几乎一致

移动语义关键在于状态的转移(比如内存的转移而非复制(其实就是把老内存的标识符和赋值符右边的状态源的标识符用swap交换,然后右边的等operator结束就会自动被析构,老状态就消失掉了)),为什么拷贝赋值运算符的效率低,主要是因为它的核心操作不是”转移”,不是把想要赋给对象的状态取过来(实际上是使位于赋值号左边的本对象的状态与位于赋值号右边的新状态的状态持有者交换状态),而是首先手动把老状态析构掉,然后进行诸如申请新内存的操作然后通过诸如把右边的数据拷贝到新内存里的操作以更新至新状态,赋值号右边的状态源对象并没有被修改,
在拷贝赋值运算符工作的过程中,产生了复制行为,所以被称为拷贝赋值

为什么统一资源分配 ( unifying assignment operator )能work ?
以StrVec为例,首先看它奇怪的pass-by-value的参数
1. 以左值作为参数调用该运算符时,将会执行该对象的拷贝构造(深拷贝)用以初始化形参ori,注意形参是个临时量,该operator结束之后形参就自动被析构了

这个”调用拷贝构造”的行为带来的性能开销是不可避免的,它对应着拷贝赋值运算符的行为,拷贝赋值运算符也有这个开销,但它比拷贝赋值运算符高明在哪里呢?
高明就高明在虽然我们拷贝了新的内容到临时形参,但是我们不必手动释放这个临时形参的资源,不必手动调用free()已完成系统析构前的一些操作,因为形参在函数调用结束后会自动被析构.而拷贝赋值运算符中必须手动进行this的状态的销毁
在初始化形参之后,我们通过定义在StrVec类内的swap函数将this和形参进行状态的转移,新状态中的三个内存标识符指向形参在堆上申请的新内存,形参的三个状态标识符指向this之前指向的老内存.而函数调用完成之后形参立即被析构,析构操作导致老状态被销毁,这一切都是系统自动完成的
由此可见,统一资源符分配能够胜任拷贝赋值运算符的工作
2. 以右值作为参数调用该运算符时,编译器会进行copy-elision ,这个优化指的是,当函数以值传递的方式传参时通常要求形参使用拷贝初始化,但是当右值被传入时则没必要,直接使用源对象即可
所以这种调用实际上在传参时就已经更快一步了,接下来的函数体和移动赋值运算符一致,右值是一个即将被销毁的值,在调用方调用统一赋值运算符的那行代码结束时,该持有左边对象的老状态的右值被销毁,老状态就消失了

猜你喜欢

转载自blog.csdn.net/u010742342/article/details/80723127
今日推荐