• c++11之右值引用


    本文大部分来自这里,并不是完全着行翻译,如有不明白的地方请参考原文。

    在c++中,创建临时对象的开销对程序的影响一直很大,比如以下这个例子:

    String getName(){
        return “Kian”;
    }
    string name = getName();

    name对象的构建可以细分为3步:

    1. 用kian构建函数内的局部string对象tmp1

    2. 调用复制构造函数将tmp1复制到tmp2,并析构tmp1.

    3. 调用赋值拷贝函数将tmp2拷贝到name,并析构tmp2。

    所以一共做了3次内存分配,两次复制拷贝操作,但是tmp1和tmp2都马上析构了,如果内存分配很大的话,这里的资源浪费是很可观的。在c++11之前,编译器已经会做一些优化了,比如返回值优化RVO(return value optimization)优化了第二步,省略了构建tmp2的开销,但是第3步直到c++11引入移动语义后才得到了彻底解决。移动语义依赖于右值引用实现,要了解移动语义,必须先要明白什么是右值和左值。

    什么是右值和左值

    c++11中存在右值和左值。左值可以取地址,是相对永久的对象,可以被赋值,比如

    int a;
    a=1; //a is a lvalue

    左值也可以不是变量,如

    int x;
    int& getRef ()
    {
        return x;
    }
    getRef() = 4;

    相应地,右值是一个临时对象,不可以取地址。

    int x;
    int getRef ()
    {
        return x;
    }
    getRef();

    getRef()的值是个右值,它不是x的引用,而是x的拷贝,是一个临时存在的对象。

    右值引用和临时对象

    在c++11之前,可以使用const引用绑定到临时对象上,

    const string& name = getName(); // ok
    string& name = getName(); // NOT ok

    不可以改变一个即将消失的对象,所以将非const引用绑定到临时对象上是不允许的。到了c++11,引进了右值引用&&,

    const string&& name = getName(); // ok
    string&& name = getName(); // also ok - praise be!

    左值和右值最重要的区别在于做为函数参数时,

    printReference (const String& str)
    {
        cout<<str;
    }
    printReference (String&&  str)
    {
         cout<<str;
    }

    前者可以接受任何参数,而后者只可以接收右值,及临时对象,

    string me( "alex" );
    printReference( me ); // calls the first printReference function, taking an lvalue reference
    printReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference

    移动构造函数和赋值符

    移动构造函数接收一个临时对象作为参数,直接获取临时对象内部资源,避免重新分配内存。

    假设我们有这样一个简单的ArrayWrapper类:

    class ArrayWrapper
    {
    public:
    ArrayWrapper (int n)
        _p_vals( new int[ n ] ),_size( n )
        { }
    // copy constructor
    ArrayWrapper (const ArrayWrapper& other)
    : _p_vals( new int[ other._size ] ), _size( other._size )
    {
        for ( int i = 0; I < _size; ++i )
        {
            _p_vals[ i ] = other._p_vals[ i ];
        }
    }
    ~ArrayWrapper ()
    {
        delete [] _p_vals;
    }
    private:
        int *_p_vals;
        int _size;
    };

    可以看出复制构造函数每次都需要分配内存并着个赋值,这是非常消耗资源的,我们加一个移动构造函数,

    // move constructor
    ArrayWrapper (ArrayWrapper&&  other)
        : _p_vals( other._p_vals ), _size( other._size )
    {
        other._p_vals = NULL;
        other._size = 0;
    }

    移动构造函数比复制构造函数简单多了,不过要注意两点,

    1. 参数必须是非const右值引用

    2. 必须将other._p_vals设为NULL

    参数不设为非const,就不能将other._p_vals设为NULL,为什么要设为NULL?,因为 other是一个即将消失的右值,调用其析构函数会释放_p_vals指向的内存,不设置为NULL,我们得到的对象就会指向垃圾内存。

    因为参数是非const的,不能接收const右值,所以千万不要这样返回需要使用的右值。

    const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!

    如果对象内部包含另一个对象,移动构造函数内会发生什么?假设ArrayWrapper包含的不只有_size,而是更详细的数据,比如

    class MetaData
    {
    public:
    MetaData (int size, const std::string& name)
        : _name( name ), _size( size )
        {}
    // copy constructor
    MetaData (const MetaData&  other)
        : _name( other._name ), _size( other._size )
        {}
    // move constructor
    MetaData (MetaData&& other)
        : _name( other._name ), _size( other._size )
        {}
    std::string getName () const { return _name; }
    int getSize () const { return _size; }
    private:
        std::string _name;
        int _size;
    };

    那么ArrayWrapper需要修改成这样,

    class ArrayWrapper
    {
    public:
    // default constructor produces a moderately sized array
    ArrayWrapper ()
        : _p_vals( new int[ 64 ] ), _metadata( 64, "ArrayWrapper" )
        {}
    ArrayWrapper (int n)
        : _p_vals( new int[ n ] ), _metadata( n, "ArrayWrapper" )
        {}
    // move constructor
    ArrayWrapper (ArrayWrapper&& other)
    : _p_vals( other._p_vals ), _metadata( other._metadata )
    {
        other._p_vals = NULL;
    }
    // copy constructor
    ArrayWrapper (const ArrayWrapper& other)
        : _p_vals( new int[ other._metadata.getSize() ] ), _metadata( other._metadata )
    {
        for ( int i = 0; i< _metadata.getSize(); ++i )
        {
            _p_vals[ i ] = other._p_vals[ i ];
        }
    }
    ~ArrayWrapper ()
    {
        delete [] _p_vals;
    }
    private:
    int *_p_vals;
    MetaData _metadata;
    };

    当 ArrayWrapper调用移动构造函数时,_metadata调用的是移动构造函数还是复制构造函数?表面看应该是移动构造函数,因为ArrayWrapper的移动构造函数参数other 是右值引用,但要注意的是,右值引用不是右值!右值引用是一个左值,other._metadata也是一个左值,所以_metadata调用的是复制构造函数。

    Std::move

    怎样让_metadata也调用移动构造函数,我们需要使用std::move,move并不移动任何东西,只是将对象转换为右值。使用move后,代码就是这样的,

    // move constructor
    ArrayWrapper (ArrayWrapper&& other)
        : _p_vals( other._p_vals ) , _metadata( std::move( other._metadata ) )
    {
        other._p_vals = NULL;
    }
    MetaData (MetaData&& other)
        : _name( std::move( other._name ) ) 
        : _size( other._size )
        {}

    move的功能很神奇吧,它是用什么新技术将对象转为右值的呢?事实是它用的是c++一直都有的static_cast转换符。下面是它的源码,

    template <typename _Tp>
    <P>inline typename ;std::remove_reference<_Tp>:::type&&
    move(_Tp&&& __t)
    {return static_cast<typename std::remove_reference<_Tp>::type&&> (__t);}

    看到它的第一感觉是它应该只能接收右值引用啊,为什么左值没有问题?

    String s1(“kian”), s2;
    s2 = std:move(string(“zhang”)); //rvalue,right
    s2 = std:move(s1); //lvalue, right

    通常我们不能将右值引用绑定到左值上,不过为了支持move语义,c++11定义了两个例外:

    1. 当将左值引用传递给函数的右值引用参数,且此右值引用指向模板类型参数(如_Tp&&),编译器推断模板类型参数为实参的左值引用类型。即在std:move(s1)中,

    _Tp推断为string&,那么string& &&又是什么?这由第二条确定例外定义

    2. 引用的引用形成折叠,x& &,x& &&, x&& &都折叠成x&;只有x&& &&折叠成x&&。

    所以std:move(s1)会实例化

    string&& move(string& t)

    而std:move(string(“zhang”))实例化

    string&& move(string&& t)

    参考:

    http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html

    http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization

  • 相关阅读:
    drf3
    字典的操作方法
    列表的操作方法
    字符串的操作方法
    while循环和基本运算符
    初识数据类型
    USDT相关
    带团队
    CentOS7更改时区及同步网络时间
    mac胡刷新dns
  • 原文地址:https://www.cnblogs.com/coderkian/p/3604191.html
Copyright © 2020-2023  润新知