C++11右值引用

何为右值?

下面两行代码中i是左值,10是右值;v是左值,getVar()返回的是个临时变量,也是右值,改临时变量在改行表达式结束后消失。

1 int i = 10;
2 int &j = getVar();
3 auto f = []{return 6;};

左值的特征,我的理解是能取地址的是左值,不能取指,生存周期仅限于某个表达式的是右值,如上面的10,我们是取不到地址的,因为该行表达式结束后变量即被释放,即使取址了也没有意义,后面没法利用这个地址在操作这个值了。右值是无名临时变量。

C++中函数返回的非引用临时变量、lamda表达式、运算表达式产生的临时变量都属于纯右值。

Why右值引用?

就是对右值的引用咯。废话。。。写法是T &&a=b;

1 int&& i = 10;    //i是对10的右值引用

C++11为何要有右值引用呢?为了解决C++98的临时变量的拷贝问题,OBJ的拷贝开销是比较大的。

很多时候有这样一个场景:一个函数要返回一个对象,或对象的引用,但这个对象是临时的,因此就不能返回引用了,只能返回OBJ,对临时变量进行拷贝,如下所示:

1 T getObj(void) 
2 {
3     T OBJ;
4     //do sth with OBJ
5     return OBJ;
6 }

右值引用很好的解决了这个问题。比较下面两行代码:

1 T obj= getObj();    //调用了T的拷贝构造函数,成本高
2 T &&objk= getObj();   //对返回对象的右值引用

上面这两行代码的唯一差别就在于前面的&&,但是语意上相差很大:第一行是熟悉的,会调用拷贝构造函数,而第二行是对返回的临时对象的右值拷贝,会对临时对象进行续命,这个临时对象和objk的声明周期是一致的。用了右值引用就减少了一次临时变量的拷贝(拷贝给返回值),一次临时变量的析构。

T&&一定是右值吗?

看如下一段代码:

1 void post(int &&t) {}
2 
3 int main()
4 {
5     post(10);    //OK
6 
7     int &&x = 10;
8     post(x);  //Error, x是左值,需要传右值。
9 }

上述代码确实让人认为只能传右值给post函数作为入参。但是当发生自动推导,也就是模版或者auto时,情况就不一样了:

 1 template <typename T>
 2 void post(T &&t) {}
 3 
 4 int main()
 5 {
 6     post(10);     //OK 
 7 
 8     int &&x = 10;
 9     post(x);   //OK
10 }

T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references)(个人认为是左值引用或者右值引用类型,虽然写了T&&),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。

Why右值引用2.0

有这样一个场景,某个函数返回临时变量OBJ,按照C++98的做法是调用拷贝构造函数,注意:如果OBJ内有指针,肯定是要深拷贝的,也就是说要重新new出一个堆来。但是对临时变量的深拷贝完成后,马上就释放了堆内存,特别是对于内存占用比较大的OBJ,不仅多余,而且严重影响性能。C++11的右值引用很好的解决了这个问题。C++11为了解决这个问题,提出了move constructor的概念,也就是移动构造函数。

 1 class A
 2 {
 3 public:
 4     A() :m_ptr(new int(0)){}
 5     A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
 6     {
 7         cout << "copy construct" << endl;
 8     }
 9     A(A&& a) :m_ptr(a.m_ptr)       // move constructor
10     {
11         a.m_ptr = nullptr;
12         cout << "move construct" << endl;
13     }
14     ~A(){ delete m_ptr;}
15 private:
16     int* m_ptr;
17 };
18 
19 A GetA()
20 {
21     return A();
22 }
23 
24 int main()
25 {
26     A a = GetA();
27 }

上面的代码并没有调用拷贝构造函数进行深拷贝,而是调用了move constructor,简单的将指针的所有权交给了新的对象,之前的指针变为null。

1 A(A&& a) :m_ptr(a.m_ptr)
2 {
3     a.m_ptr = nullptr;
4     cout << "move construct" << endl;
5 }

这里怎么知道要调用move constructor而不是copy constructor的呢?这里并没发生类型推导,因为都是明确的类型。所以入参必须是右值,否则编译报错。函数返回值属于右值,所有调用了move constructor了。试问:左值也想要使用move constructor呢?C++11提供了std::move,将左值转换为右值,便于使用move constructor。需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全。

std::move

1 {
2     std::list< std::string> tokens;
3     //省略初始化...
4     std::list< std::string> t = tokens; //这里存在拷贝 
5 }
6 std::list< std::string> tokens;
7 std::list< std::string> t = std::move(tokens);  //这里没有拷贝 

如果不用std::move,拷贝的代价很大,性能较低。std::move避免了不必要的深拷贝。

std::move只是将左值左了cast_to_rvalue操作然后显式的调用移动构造函数,对于右值,直接使用std::move显式调用移动构造函数。并不是所有情况用std::move都好的,比如int和char[10]定长数组等类型,使用move的话仍然会发生拷贝,因为他没有对应的移动构造函数。对于含有句柄或是指针的对象(也就是需要深拷贝的对象),std::move操作更有意义。说到这里,个人认为右值引用的根本目的就是减少不必要的深拷贝,而不是避免拷贝。对于浅拷贝,右值引用意义不大。

std::forward<T>

之前说到过,模版自动推导时或者auto时T&&接收的参数不一定时右值,也有可能时右值,原本型参是左值的,传进来后也变成右值了。如下实验:

void proccessValue(int &i)
{
    cout<<"rvalue"<<endl;
}

void proccessValue(int &&i)
{
    cout<<"lvalue"<<endl;
}

template <typename T>
void forwardValue(T &&i)
{

    proccessValue(i);   //i变成了右值,不管传进来之前是左还是右
}

int main()
{
    int i = 10;
    forwardValue(10);
    forwardValue(i);
} 

 这就造成了后续使用的困惑了。为了保持型参原来的左右属性,使用std::forward<T>

1 template <typename T>
2 void forwardValue(T &&i)
3 {
4 
5     proccessValue(std::forward<T>(i));
6 }

overall

右值引用解决了无谓的拷贝构造函数的开销。

移动构造函数(move constructor)告诉编译器如何进行拷贝右值对象,避免了深拷贝的开销。

左值也想使用右值引用的特性,所有C++11提供了std::move函数,将左值cast_to_rvalue了。

类型推导时传给T&&后导致左值变成了右值,此时想利用其原始的左值属性的话,C++11又提供了std::forward<T>()。

本文也参考了:从4行代码看右值引用

 

posted @ 2018-05-26 00:57  guhowo  阅读(211)  评论(0编辑  收藏  举报