第14课 移动语义(std::move)

摘要:
使用namespacestd//1移动移动语义#include#include。移动语义类HugeMemo{public:int*buff;intsize;HugeMemo:size(大小˃0?大小:1){buff=newint[size];}//移动构造函数HugeMemooneexcept:size,buff{hm.buff=nullptr;}~ HugeMemo(){delete[]buff;};ClassMoveable{public:HugeMemoh;int*i;public:Moveable():i,h{}//移动构造函数(强制到右边的值以调用h的移动构造函数。Moveablenonexcept:i(m.i),h{m.i=nullptr;}~Moveable(){deletei;}};MoveableGetTemp(){Moveabletmp=Moveable();cout˂˂hex˂˂“Hugememfrom”˂˂__func___˂“@”˂˂tmp.h.buff˂˂endl;returntmp;}//2。移动常量对象将成为复制操作类Annotation{std::stringvalue;public://Note:对常量文本对象执行移动操作时,因为std::move返回的结果是一个//conststd::string对象,因为它有const,所以它不能匹配字符串移动构造函数,//但它匹配字符串复制构造函数,所以当执行值//时,文本实际上被复制到value。例如:1。std::vector和std:array。

一. std::move

(一)std::move的原型

 template<typename T>
 decltype(auto) move(T&& param)  //注意,形参是个引用(万能引用)
{
        using ReturnType = typename remove_reference<T>::type&&; //去除T自身可能携带的引用
        return static_cast<ReturnType>(param); //强制转换为右值引用类型
}

(二)注意事项

    1. std::move的本质就强制类型转换,它无条件地将实参转为右值引用类型匿名对象,是个右值),继而用于移动语义。

    2. 该函数只是将实参转为右值,除此之外并没有真正的move任何东西。实际上,它在运行期没任何作为,编译器也不会为它生成任何的可执行代码,连一个字节都没有。

    3. 如果要对某个对象执行移动操作时,则不要将其声明为常量。因为针对常量对象执行移动操作将变成复制操作

二. 移动语义

(一)深拷贝和移动的区别

第14课 移动语义(std::move)第1张 

  1. 深拷贝:将SrcObj对象拷贝到DestObj对象,需要同时将Resourse资源也拷贝到DestObj对象去。这涉及到内存的拷贝。

  2. 移动:通过“偷”内存的方式,将资源的所有权从一个对象转移到另一个对象上但只是转移,并没有内存的拷贝。可见Resource的所有权只是从SrcObj对象转移到DestObj对象,由于不存在内存拷贝,其效率一般要高于复制构造。

(二)复制和移动操作函数

   1. 复制/移动操作的函数声明

①Object(T&);       //复制构造,仅接受左值
②Object(const T&); //复制构造,即可以接受左值又可接收右值
③Object(T&&) noexcept; //移动构造,仅接受右值
④T& operator=(const T&);//复制赋值函数,即可以接受左值又可接收右值
⑤T& operator=(T&&); //移动赋值函数,仅接受右值

   2. 注意事项

  ①移动语义一定是要修改临时对象的值,所以声明移动构造时应该形如Test(Test&&),而不能声明为Test(const Test&&)

  ②默认的移动构造函数实际上跟默认的拷贝构造函数一样,都是“浅拷贝”。通常情况下,必须自定义移动构造函数。

  ③对于移动构造函数来说,抛出异常是很危险的。因为移动语义还没完成,一个异常就抛出来,可能会造成悬挂指针。因此,应尽量通过noexcept声明不抛出异常,而一旦出现异常就可以直接调用std::terminate终止程序。

  ④特殊成员函数之间存在相互抑制的生成机制,可能会影响到默认拷贝构造和默认移动构造函数的自动生成。(详见《特殊成员函数的生成机制》一节)

【编程实验】move移动语义

#include <iostream>
#include <vector>

using namespace std;

//1. 移动语义
class HugeMem
{
public:
    int* buff;
    int size;

    HugeMem(int size) : size(size > 0 ? size : 1)
    {
        buff = new int[size];
    }

    //移动构造函数
    HugeMem(HugeMem&& hm) noexcept : size(hm.size), buff(hm.buff)
    {
        hm.buff = nullptr;
    }

    ~HugeMem() { 
        delete[] buff;
    }
};

class Moveable
{
public:
    HugeMem h;
    int* i;
public:
    Moveable() : i(new int(3)), h(1024){}

    //移动构造函数(强制转为右值,以调用h的移动构造函数。注意m虽然是右值
    //引用,但形参是具名变量,m是个左值。因此m.h也是左值,需转为右值。
    Moveable(Moveable&& m) noexcept: i(m.i), h(std::move(m).h)
    {
        m.i = nullptr;
    }

    ~Moveable() { delete i; }
};

Moveable GetTemp()
{
    Moveable tmp = Moveable();

    cout << hex << "Huge mem from " << __func__
        << " @" << tmp.h.buff << endl;

    return tmp;
}

//2. 对常量对象实施移动将变成复制操作
class Annotation
{
    std::string value;
public:

    //注意:对常量的text对象实施移动操作时,由于std::move(text)返回的结果是个
    //const std::string对象,由于带const,不能匹配string(&& rhs)移动构造函数,
    //但匹配string(const string& rhs)复制构造函数,因此当执行value(std::move(text))
    //时,实际上是将text复制给value。对于非string类型的情况也一样,因此对常量对象的
    //移动操作实际上会变成复制操作!
    explicit Annotation(const std::string text) : value(std::move(text))
    {
    }
};

//3. 利用移动语义实现高性能的swap函数
template<typename T>
void Swap(T& a, T& b) noexcept  //声明为noexcept以便在交换失败时,终止程序
{
    //如果a、b是可移动的,则直接转移资源的所有权
    //如果是不可移动的,则通过复制来交换两个对象。
    T tmp(std::move(a)); //先把a的资源转交给tmp
    a = std::move(b);
    b = std::move(tmp);
}

int main()
{
    //1. 移动语义
    Moveable a(GetTemp()); //移动构造

    cout << hex << "Huge mem from " << __func__
        << " @" << a.h.buff << endl;

    return 0;
}
/*输出结果
Huge mem from GetTemp @02C66248 (从中可以看出Huge mem从临时对象移动了a对象)
Huge mem from main @02C66248
*/

三、正确理解移动语义

(一) “移动”操作实际上是一种请求,因为有些类型不存在移动操作,对于这些对象会通过其复制操作来实现“移动”。

(二)某些类型的移动操作未必比复制操作更快。如:

  1. std::vector和std::array。

第14课 移动语义(std::move)第2张 

  (1)标准库大部分容器类(如vector),内部是将其元素Widgets存放在堆上,然后用指针指向该堆内存。在进行移动操作时,只是进行指针的复制。整个容器内容在常数时间内便可移动完成。

  (2)而std::array对象缺少这样的一根指针,因为其内容数据是直接存储对象上的。虽然std::array提供移动操作,但其移动和复制的速度哪个更快,取决于元素Widget的移动和复制速度的比较。同时std::array移动时需要对每一个元素进行移动,总是需要线性时间。

  2. 许多std::string类型的实现采用了小型字符串优化(SSO)。当使用SSO后,“小型”字符串(如不超过15个字符)会存储在std::string对象内的某个缓冲区内,即内容直接存储在对象上(而不是堆上)。因此,此时是整个对象的移动,速度并比复制更快。

(三)标准库一些容器操作提供了强异常安全保证,为了兼容C++98的遗留代码在升级到C++11时仍保证正确性。库中用std::move_if_noexcept模板来替代move函数。该函数在类的移动构造函数没有声明noxcept关键字时返回一个左值引用从而使变量通过拷贝语义,而在移动构造函数有noexcept时返回一个右值引用,从而使变量可以使用移动语义。移动操作未加noexcept时,编译器仍会强制调用一个复制操作

【编程实验】正确理解移动语义

#include <iostream>
#include <chrono>
#include <vector>
#include <array>
#include <thread>

using namespace std;

//1. 移动不存在时,实行的是复制操作
class Foo
{
public:
    Foo(){}
    Foo(const Foo&)
    {
        cout <<"Foo(const Foo&)" << endl;
    }
};

//2. 移动速度未必比复制快
//2.1 辅助类(元素类)
class Widget
{
public:
    Widget() = default;
    Widget(const Widget&) {
        //模拟复制操作,假设需要1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    Widget(Widget&&) {
        //模拟移动操作,假设需要2毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(2));
    }

    Widget& operator=(const Widget&) {
        //模拟复制赋值操作,假设需要1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        return *this;
    }
    Widget& operator=(Widget&&) {
        //模拟移动赋值操作,假设需要2毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(2));
        return *this;
    }
};

//2.2. 计算任意函数的执行时间:auto&&用于lambda表达式形参(C++14)
auto funcTimer = [](auto&& func, auto&& ... params)
{
    //计时器启动
    std::chrono::system_clock::time_point t1 = std::chrono::system_clock::now();

    //调用func(param...)函数
    std::forward<decltype(func)>(func)(           //根据func的左右值特性来调用相应的重载&或&&版本的成员函数
        std::forward<decltype(params)>(params)... //保持参数的左/右值特性
        );

    std::chrono::system_clock::time_point t2 = std::chrono::system_clock::now();
    long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
    cout << elapsed << " microseconds" << endl;
};
//2.3 复制和移动操作
auto lamMove = [](auto&& src) {
    auto dest = std::move(src);
    return;
};

auto lamCopy = [](auto&& src) {
    auto dest = src;
    return;
};

//2.4 测试vector类
void testVector()
{
    std::vector<Widget> vw1{ 10,Widget() };

    cout <<"copy vector: " ;
    funcTimer(lamCopy, vw1);

    //测试移动操作用时
    cout << "move vector: ";
    funcTimer(lamMove, vw1);
}

//2.5 测试array类
void testArray()
{
    std::array<Widget, 10> aw1;

    cout << "copy array: ";
    funcTimer(lamCopy, aw1);

    //测试移动操作用时
    cout << "move array: ";
    funcTimer(lamMove, aw1);
}

//3. move_if_noexcept的用法
struct Maythrow
{
    Maythrow() {}

    Maythrow(const Maythrow&) {
        cout <<"Maythrow copy construct." << endl;
    }

    Maythrow(Maythrow&&) {
        cout << "Maythrow move construct." << endl;
    }
};

struct Nothrow
{
    Nothrow() {}

    Nothrow(const Nothrow&) {
        cout << "Nothrow copy construct." << endl;
    }

    Nothrow(Nothrow&&) noexcept {  //注意,这里声明为noexcept!
        cout << "Nothrow move construct." << endl;
    }
};
int main()
{
    //1. 移动操作不存在时
    Foo f1;
    Foo f2 = std::move(f1); //调用复制构造函数

    //2. 移动速度未必比复制快
    testVector();
    testArray();

    //3. 移动未声明为noexcept时,调用复制构造
    Maythrow m;
    Nothrow  n;

    Maythrow mt = move_if_noexcept(m); //move_if_noexcept返回左值引用,调用复制构造函数
    Nothrow  nt = move_if_noexcept(n); //move_if_noexcept返回右值引用,调用移动构造函数

    return 0;
}
/*输出结果
Foo(const Foo&)
copy vector: 19825 microseconds
move vector: 5 microseconds     //常量时间
copy array: 19109 microseconds
move array: 29589 microseconds  //移动的速度未必比复制快!取决于Widget的移动和复制速度的比较!
Maythrow copy construct. //调用复制构造函数
Nothrow move construct.  //调用移动构造函数
*/

免责声明:文章转载自《第14课 移动语义(std::move)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇ProgressBar 进度条 旋转WPF DataGrid 绑定DataSet数据 自动生成行号下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

C++ 常见崩溃问题分析

一、前言 从事自动化测试平台开发的编程实践中,遭遇了几个程序崩溃问题,解决它们颇费了不少心思,解决过程中的曲折和彻夜的辗转反侧却历历在目,一直寻思写点东西,为这段难忘的经历留点纪念,总结惨痛的教训带来的经验,以期通过自己的经历为他人和自己带来福祉:写出更高质量的程序; 由于 C 和 C++ 这两种语言血缘非常近,文本亦对 C 编程语言有借鉴作用; 二、C+...

函数中参数传递的5种方式

1、必须参数(位置参数)      必需参数:先用形式参数定义,然后在调用时对应位置使用实参(具体的数值)调用,定义的形式参数和调用的实际参数必需一一对应(顺序、数量)。       def sum(a, b):           return a + b       sum(5, 6) 2、关键字参数      关键字参数:先使用形式参数定义,然后调用...

XPO 学习资料(转摘)

Dev 出品的XPO是一个O/R Mapping框架,虽然是商业软件,非开源,但提供了源码。况且Dev的产品一向以精品为主,值得好好研究一下(我不是Dev的代理)。 于是在学习过程中做个摘要。   一、一个持久类(Persistent Class)一般来说映射为一个表类型,该表的每一条纪录即一个持久类的实例。 持久类继承自XPObject或者XPBaseO...

golang的传值调用和传引用调用

传值还是传引用 调用函数时, 传入的参数的 传值 还是 传引用, 几乎是每种编程语言都会关注的问题. 最近在使用 golang 的时候, 由于 传值 和 传引用 的方式没有弄清楚, 导致了 BUG. 经过深入的尝试, 终于弄明白了 golang 的 传值 的 传引用, 尝试过程记录如下, 供大家参考! golang 本质上都是传值方式调用 严格来说, go...

解析 Linux 中的 VFS 文件系统机制(根文件系统的由来)

http://os.51cto.com/art/200803/67283_4.htm ◆1. 摘要 本文阐述 Linux 中的文件系统部分,源代码来自基于 IA32 的 2.4.20 内核。总体上说 Linux 下的文件系统主要可分为三大块:一是上层的文件系统的系统调用,二是虚拟文件系统 VFS(Virtual Filesystem Switch),三是...

Python 多线程库总结

多线程库总结基于线程的并行性threading模块下面是一些基础函数,函数包括: 函数 threading.active_count() threading.current_thread() threading.get_ident() threading.enumerate() threading.main_thread() threading.se...