您的位置:首页 > 编程语言 > C语言/C++

C++11 学习笔记 右值引用

2015-12-15 09:59 369 查看
一.右值引用

C++11增加了一个新的类型,称为右值引用(R-value reference),标记为T &&。右值是指表达式结束后就不再存在的临时对象。相对应的左值就是指表达式结束后依然存在的持久对象,所有的具名变量或对象都是左值,而右值不具名。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值。

在C++11中,右值由两个概念构成,一个是将亡值(xvalue,expiring value)(C++11新增的,与右值引用相关的表达式,比如将要被移动的对象,T&& 函数返回值,std::move返回值和转换为T&&的类型的转换函数的返回值),另一个是纯右值(rvalue, PureRvalue)(非引用返回的临时变量,运算表达式产生的临时言火日王,原始字面量和lambda表达式等都是纯右值。C++11中所有的值必属于左值,将亡值,纯右值三者之一。

1.&&的特性

与左值引用相类似,右值引用就是对右值进行引用的类型。因为右值不具名,所以,我们只能通过引用的方式找到它。

1).声明右值引用时必须立即进行初始化。因为引用类型并不拥有所绑定对象的别名,只是该对象的一个别名。

2).通过右值引用的声明,该右值的生命周期将会与右值引用类型变量的生命周期一样,只要变量还在,右值就会一直存活。

#include <iostream>

using namespace std;

int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;

struct A
{
     A(){
         cout<<"construct: "<<++g_constructCount<<endl;
     }

     A(const A& a){
         cout<<"copy construct: "<<++g_copyConstructCount<<endl;
     }

     ~A(){
         cout<<"destruct: "<<++g_destructCount<<endl;
    }
};

A GetA(){
     return A();
}

int main(){
     A a = GetA();
     return 0;
}
在关闭返回值优化的情况下,输出结果:

construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
拷贝构造函数调用了两次:

1).GetA()函数内部创建的对象返回后构造一个临时对象时调用。

2).在main函数中构造a对象时调用。

优化(右值引用绑定了右值,让临时右值的生命周期延长了):

int main(){
     A&& a = GetA();
     return 0;
}

//输出结果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
在C++98/03中,通过常量左值引用也经常用来做性能优化,输出结果与右值引用一样。因为常量左值引用是一个“万能”的引用类型,可以接受左值,右值,常量左值和常量右值。

实际上,T&&并不是一定表示右值,它绑定的类型是未定的,即可能是左值又可能是右值。

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

f(10);                 //param是右值
int x = 10;
f(x);                   //param是左值


在像上面这样的例子中,当发生自动类型推导时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references(未定的引用类型),它是左值还是右值引用取决于它的初始化。 需要注意的是,有一个很关键的规则:universal references仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。

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

template<typename T>
class Test{
     ...
     Test(Test&& hrs);          //右值引用
     ...
};

void f(Test&& param);         //右值引用


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

temple <typename T>
void f(const T&& param);             //右值引用


记住,如果不是universal references,用一个左值初始化一个右值引用类型是不合法的。

正确的做法是使用std::move将一个左值转换成右值。

int w1;
decltype(w1)&& v1 = w1;    //error

decltype(w1)&& v1 = std::move(w2);


编译器会将己命名的右值引用视为左值,而将未命名的右值引用视为右值。

void PrintValue(int& i){
     std::cout<<"lvalue : "<<i<<std::endl;
}

void PrintValue(int&& i){
     std::cout<<"rvalue : "<<i<<std::endl;
}

void Forward(int&& i){
    PrintValue(i);
}

int main(){
    int i=0;
    PrintValue(i);
    PrintValue(1);
    Forward(2);
}

输出结果:
lvalue : 0
rvalue : 1
lvalue : 2


2.右值引用优化性能,避免深拷贝(C++11加入右值引用的原因)

对于含有堆内存的类,我们都需要提供其深拷贝的构造函数,否则,会使用其默认提供的拷贝构造函数,容易导致堆内存的重复删除,指针指向为空。

class A
{
public:
     A():m_ptr(new int(0)){}
     ~A(){
         delete m_ptr;
     }
private:
     int* m_ptr;
};

A get(bool flag){
     A a;
     A b;
     if(flag){
          return a;
     }else{
          return b;
     }
}

int main(){
     A a = Get(false);  //临时变量的m_ptr指向为空,析构时,重复删除引起错误...
}
下面是正确的做法,提供了深拷贝的拷贝构造函数

class A
{
public:
     A() : m_ptr(new int(0)){
          cout<<"constructor"<<endl;
     }
     A(const A& a):m_ptr(new int(*a.m_ptr)){
          cout<<"copy construct"<<endl;
     }

     ~A(){
          cout<<"destruct"<<endl;
          delete m_ptr;
     }

private:
     int* m_ptr;
};

int main(){
    A a = Get(false);
}

输出结果:
construct
construct
copy construct
destruct
destruct
destruct


这样虽然是安全的,但是却因为拷贝构造带来了额外的损耗。

Get函数会返回临时变量,然后通过临时变量拷贝构造一个新的对象b,临时变量在拷贝构造完成之后销毁了,如果堆内存很大,那么这个拷贝构造的代价会很大。因为可以使用移动构造函数(对右值引用进行浅拷贝)。

class A
{
public:
     A():m_ptr(new int(0)){
          cout<<"construct"<<endl;
     }

     A(const A& a):m_ptr(new int(*a.m_ptr)){
          cout<<"copy construct"<<endl;
     }

     A(A&& a) : m_ptr(a.m_ptr){
           a.m_ptr = nullptr;
           cout<<"move construct: "<<endl;
     }

     ~A(){
           cout<<"destruct"<<endl;
           delete m_ptr;
     }

private:
     int* m_ptr;
};

int main(){
     A a = Get(false);
}

//输出结果
construct
construct
move construct
destruct
destruct
destruct
移动构造函数中,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。右值引用的一个重要的目的是用来支持移动语义的。

下面看一个MyString类实现的例子

class MyString{
private:
     char * m_data;
     size_t m_len;
     
     void copy_data(const char *s){
          m_data = new char[m_len+1]; 
          memcpy(m_data, s, m_len);
          m_data[m_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)
                 delete[] m_data;
     }
};

int main(){
      MyString a;
      a = MyString("Hello");
      std::vector<MyString> vec;
      vec.push_back(MyString("World"));
      return 0;
}


//MyString的移动构造函数和移动赋值函数
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;
}


3.move语义

移动语义是通过右值引用来匹配临时值的,普通的左值该怎么办呢?C++11提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象的内存或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝,将一个左值强制转换为一个右值引用。

std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens);

使用了move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝,提高了性能。

4.forward和完美转发

一个右值引用参数作为函数的形参,在函数内部再转发该参数时变成了一个左值,不是原来的类型了。

template <typename T>
void forwardValue(T& val){
     processValue(val);                   //右值参数会变成左值
}

template <typename T>
void forwardValue(const T& val){
     processValue(val);                 //参数都变成常量左值引用了
}

因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发称为完美转发。C++11提供了一个函数std::forward,为转发而生,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,都会按照参数本来的类型转发。

void Print(int& t){
     cout<<"lvalue"<<endl;
}

template <typename T>
void PrintT(int &t){
     cout<<"rvalue"<<endl;
}

template <typename T>
void TestForward(t && v){
     PrintT(v);
     PrintT(std::forward<T>(v));
     PrintT(std::move(v));
}

Test(){
     TestForward(1);
     int x=1;
     TestForward(x);
     TestForward(std::forward<int>(x));
}

//输出结果:
lvalue
rvalue
rvalue


5.emplace_back减少内存拷贝和移动

emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比push_back能更好地避免内存的拷贝和移动,使容器查入元素的性能得到进一步提升。在大多数情况下应该优先使用emplace_back来代替push_back。所有的标准库容器(array除外,因为它长度不可以变,不能插入元素)都增加了类似的方法:emplace,emplace_hint,emplace_front,emplace_after和emplace_back。

#include <vector>
#include <iostream>

using namespace std;

struct A
{
     int x;
     double y;
     A(int a, double b):x(a),y(b){}
};

int main(){
     vector<A> v;
     v.emplace_back(1,2);
     cout<<v.size()<<endl;
     return 0;
}
emplace_back的用法比较简单,直接通过构造函数的参数就可以构造对象,因此,也要求对象必须有对应的构造函数。如果没有,编译器会报错。

#include <vector>
#include <map>
#include <string>
#include <iostream>

using namespace std;

struct Complicated
{
     int year;
     double country;
     std::string name;

     Complicated(int a, double b, string c):year(a),country(b),name(c){
          cout<<"is constucted"<<endl;
     }

     Complicated(const Complicated& other):year(other.year),country(other.country),name(std::move(other.name)){
          cout<<"is moved"<<endl;
     }
};

int main(){
     std::map<int, Complicated> m;
     int anInt = 4;
     double aDouble = 5.0;
     std::string aString = "C++"
     cout<<"--insert--"<<endl;
     m.insert(std::make_pair(4,Complicated(anInt, aDouble, aString)));

     cout<<"--emplace--"<<endl;
     m.emplace(4, Complicated(anInt, aDouble, aString));
     
     cout<<"--emplace_back--"<<endl;
     vector<Complicated> v;
     v.emplace_back(anInt, aDouble, aString);
     cout<<"--push_back--"<<endl;
     v.push_back(Complicated(anInt, aDouble, aString));

     return 0;
}

//输出结果:
--insert--
is constructed
is moved
is moved
--emplace--
is constructed
is moved
--emplace_back--
is constructed
--push_back--
is constructed
is moved
is moved
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: