当前位置:   article > 正文

C++左值与右值以及std::move详解_cpp 左值及右值 std::move

cpp 左值及右值 std::move

1、左值与右值

这里首先要区分左值引用、右值引用和左值、右值的关系

  1. int i = 1; int& a = i; 这里a是左值,其decltype(a)为int&,为左值引用,只能等于左值
  2. int&& b = 1; b为左值,但是其是右值引用,只能等于右值
  3. fun(T&),只能接收左值,不管其类型是左值引用还是右值引用
  4. fun(T&&),如果是万能引用则左值或者右值都能接收,但是如果确定T为int,则只能接收右值
  5. 左值是可以放在等号左边的,比如*p,或者vector()[],是可以被赋值的
  6. 右值只能放在等号右边,比如函数返回值为int,其只能在等号右边
  7. 左值可以取地址,右值不可取地址
    左值具名,对应指定内存域,可访问;右值不具名,不对应内存域,不可访问。临时对像是右值。左值可处于等号左边,右值只能放在等号右边。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。
  8. 左值可以在等号右边,可以在左边,但是右值必须在等号右边
int a;  
a = 1; // here, a is an lvalue

int x;  
int& getRef ()   
{  
        return x;  
}  
getRef() = 4;  //getRef()返回的不是临时变量,是lvalue
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

其实左值就是指一个拥有地址的表达式。换句话说,左值指向的是一个稳定的内存空间(即可以是在堆上由用户管理的内存空间,也可以是在栈上,离开了一个block就被销毁的内存空间)。上面第二个例子,getRef返回的就是一个全局变量(建立在堆上),所以可以当做左值使用。

int x;  
int getVal ()  
{  
    return x;  
}  
getVal()=4; //error ,getRef()返回的是临时对象,是rvalue,不能做左值 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2、&&的特性

2.1 概述

特别注意:&&本身是为了约束等号右边的变量!例如int&& p = a;&&是约束a为右值,而与p无关,p为左值
实际上T&&并不是一定表示右值引用,它的引用类型是未定的,即可能是左值有可能是右值。看看这个例子:

ep1

template<typename T>
void f(T&& param);

f(10); //10是右值
int x = 10;
f(x); //x是左值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ep2

&&实际上是一个未定的引用类型。这个未定的引用类型被scott meyers称为universal references(可以认为它是种通用的引用类型),它必须被初始化,它是左值应用还是右值引用取决于它的初始化,如果&&被一个左值初始化的话,它就是一个左值引用;如果它被一个右值初始化的话,它就是一个右值引用。&&为universal references时的唯一条件是有类型推断发生,universal references仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个右值引用。一般运用于函数模板

template<class T>
void printT(T& a)
{
    cout<<"lvalueof&= "<<a<<endl;
}
template<class T>
void printT(T&& a)
{
    cout<<"Rvalueof&&= "<<a<<endl;
}
template<class T>
void testForward(T&& a)
{
    printT(a);//这里必须注意,a为左值,所以肯定走的printT(T& a)
    printT(std::forward<T>(a));
    printT(std::move(a));
}

int i = 1;
输入——testForward(i)
lvalueof&= 1
lvalueof&= 1
Rvalueof&&= 1
输入——testForward(22)
lvalueof&= 22
Rvalueof&&= 22
Rvalueof&&= 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

这里即是T&&失效的情况,因为函数模板testforward,以及将T实例为int,所以无需推导,已经确定,所以void fun3(T&& c)中为右值引用,如果int i=1,forwa.fun3(i)会报错,因为i为左值

template<class T>
class testforward
{
public:
    void fun1(T&& a)
    {
        cout<<"test1"<<endl;
    }
    void fun1(T& b)
    {
        cout<<"test2"<<endl;

    }
    void fun3(T&& c)
    {
        fun1(c);
        fun1(std::forward<T>(c));
    }
};
输入:testforward<int> forwa;
    forwa.fun3(3);
得到:
	test2
	test1

template<typename T>
void f(std::vector<T>&& param); //同理右值

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

2.2 &&注意事项

放在等号左边的变量本身一定是左值,但是其可以是右值引用类型

  1. 所有的右值引用叠加到右值引用上变成一个右值引用
  2. 所有的其它引用类型叠加都变成一个左值引用
  3. 左值或者右值是独立于它的类型的,也就是说一个右值引用类型的左值是合法的。
int&& var1 = x; // var1 is of type int&& (no use of auto here)
auto&& var2 = var1; // var2 is of type int& ,var2的类型是universal references(有类型推导)
  • 1
  • 2

1. 这里var1由于是int&& 所以是右值引用,要求等号右边必须为右值,但是其本身是左值
2. 这里var2由于是auto&&,需要结合等号右边推导,var1是左值(int),所以var2位int&型,但是其本身属性为左值!!!

int w1, w2;
auto&& v1 = w1;  //w1 is lvalue, v1 is lvalue, type is int&
decltype(w1)&& v2 = w2; //error, w2 is lvalue, v2 is rvalue, type is int&&

用std::move,decltype(w1)&& v2 = std::move(w2); std::move可以将一个左值转换成右值
  • 1
  • 2
  • 3
  • 4
  • 5

2.3针对右值引用的左值分析(int&& p=a(),p为左值)

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;  
};  

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;  
};  

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

注意这里的 == _metadata( other._metadata ) ==,因为 ArrayWrapper (ArrayWrapper&& other) 中,other为左值,所以other._metadata为左值,所以不会走move构造,需要改成std::move(other,_metadata)

  ArrayWrapper (ArrayWrapper&& other)  
      : _p_vals( other._p_vals  )  
      , _metadata( std::move( other._metadata ) )  
  {  
      other._p_vals = NULL;  
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.右值拷贝,可以优化内存

右值引用优化性能,避免深拷贝
如果使用std::move(a),则代表a不会再复用,将左值变成右值。变成临时变量后,其他地方将不会再用!生命即将销毁!(这里有误解,表示a不会再用,但是不改变a的生命周期!),即std::move不改变对象的生命周期,只是简单的左值变右值。关于这点可参考如下例子。

#include <future>
#include <iostream>

struct MoveOnly
{
    MoveOnly(int v_)
    : v(v_)
    {
        std::cout << ((void*)this) << " MoveOnly " << v << "\n";
    }

    ~MoveOnly()
    {
        std::cout << ((void*)this) << " ~MoveOnly " << v << "\n";
    }

    MoveOnly(const MoveOnly&) = delete;
    MoveOnly& operator=(const MoveOnly&) = delete;

    MoveOnly(MoveOnly &&src)
    {
        v = std::exchange(src.v, -1);
        std::cout << ((void*)this) << " MoveOnly&& " << v << "\n";
    }

    MoveOnly& operator=(MoveOnly&&) = default;

    MoveOnly&& Apply()
    {
        std::cout << ((void*)this) << " Apply " << v << "\n";
        return std::move(*this);
    }

    int v;
};


int main()
{
    
    MoveOnly mm = MoveOnly(1);
    std::move(mm).Apply();
    std::cout<<"?????"<<std::endl;
    
    
}



0x7fff65a4274c MoveOnly 1
0x7fff65a4274c Apply 1
?????
0x7fff65a4274c ~MoveOnly 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

针对move,c++11 增加了转移构造函数,传入右值走转移构造,可以节约空间

class DataOnly {
public:
    DataOnly ()                  // default constructor
    ~DataOnly ()                 // destructor

    DataOnly (const DataOnly & rhs)              // copy constructor
    DataOnly & operator=(const DataOnly & rhs)    // copy assignment operator

    DataOnly (const DataOnly && rhs)         // C++11, move constructor
    DataOnly & operator=(DataOnly && rhs)    // C++11, move assignment operator
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。消除了临时对象的维护 ( 创建和销毁 ) 对性能的影响。

以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。

class MyString { 
 private: 
  char* m_data; 
  size_t   m_len; 
  void copy_data(const char *s) { 
    m_data = new char[m_len+1]; 
    memcpy(_data, s, m_len); 
    m_data[_len] = '\0'; 
  } 
 public: 
  MyString() { 
    m_data = NULL; 
    m_len = 0; 
  } 

  MyString(const char* p) { 
    m_len = strlen (p); 
    copy_data(p); 
  } 

  MyString(const MyString& str) { 
    m_len = str.m_len; 
    copy_data(str.m_data); 
    std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; 
  } 

  MyString& operator=(const MyString& str) { 
    if (this != &str) { 
      m_len = str.m_len; 
      copy_data(str._data); 
    } 
    std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; 
    return *this; 
  } 

  virtual ~MyString() { 
    if (m_data) free(m_data); 
  } 
 }; 


void test() { 
  MyString a; 
  a = MyString("Hello"); 
  std::vector<MyString> vec; 
  vec.push_back(MyString("World")); 
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

MyString(MyString&& str) { 
    std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
    _len = str._len; 
    _data = str._data; //避免了不必要的拷贝
    str._len = 0; 
    str._data = NULL; 
 }
MyString& operator=(MyString&& str) { 
    std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
    if (this != &str) { 
      _len = str._len; 
      _data = str._data; //避免了不必要的拷贝
      str._len = 0; 
      str._data = NULL; 
    } 
    return *this; 
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

析构函数加判断是否为NULL,是就不用再delete!

muduo代码实例

  T take()
  {
    MutexLockGuard lock(mutex_);
    // always use a while-loop, due to spurious wakeup
    while (queue_.empty())
    {
      notEmpty_.wait();
    }
    assert(!queue_.empty());
    T front(std::move(queue_.front())); //这里用了move,因为queue_.front()马上要被pop掉,所以使用move,节省资源开销
    queue_.pop_front();
    return front;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/640019
推荐阅读
  

闽ICP备14008679号