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).通过右值引用的声明,该右值的生命周期将会与右值引用类型变量的生命周期一样,只要变量还在,右值就会一直存活。
1).GetA()函数内部创建的对象返回后构造一个临时对象时调用。
2).在main函数中构造a对象时调用。
优化(右值引用绑定了右值,让临时右值的生命周期延长了):
实际上,T&&并不是一定表示右值,它绑定的类型是未定的,即可能是左值又可能是右值。
在像上面这样的例子中,当发生自动类型推导时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references(未定的引用类型),它是左值还是右值引用取决于它的初始化。 需要注意的是,有一个很关键的规则:universal references仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。
记住,如果不是universal references,用一个左值初始化一个右值引用类型是不合法的。
正确的做法是使用std::move将一个左值转换成右值。
编译器会将己命名的右值引用视为左值,而将未命名的右值引用视为右值。
2.右值引用优化性能,避免深拷贝(C++11加入右值引用的原因)
对于含有堆内存的类,我们都需要提供其深拷贝的构造函数,否则,会使用其默认提供的拷贝构造函数,容易导致堆内存的重复删除,指针指向为空。
这样虽然是安全的,但是却因为拷贝构造带来了额外的损耗。
Get函数会返回临时变量,然后通过临时变量拷贝构造一个新的对象b,临时变量在拷贝构造完成之后销毁了,如果堆内存很大,那么这个拷贝构造的代价会很大。因为可以使用移动构造函数(对右值引用进行浅拷贝)。
下面看一个MyString类实现的例子
3.move语义
移动语义是通过右值引用来匹配临时值的,普通的左值该怎么办呢?C++11提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象的内存或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝,将一个左值强制转换为一个右值引用。
使用了move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝,提高了性能。
4.forward和完美转发
一个右值引用参数作为函数的形参,在函数内部再转发该参数时变成了一个左值,不是原来的类型了。
因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发称为完美转发。C++11提供了一个函数std::forward,为转发而生,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,都会按照参数本来的类型转发。
5.emplace_back减少内存拷贝和移动
emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比push_back能更好地避免内存的拷贝和移动,使容器查入元素的性能得到进一步提升。在大多数情况下应该优先使用emplace_back来代替push_back。所有的标准库容器(array除外,因为它长度不可以变,不能插入元素)都增加了类似的方法:emplace,emplace_hint,emplace_front,emplace_after和emplace_back。
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
相关文章推荐
- C++拷贝构造函数详解
- C语言版本opencv中CvArr和CvMat和IplImage之间关系
- C++到JAVA的痛苦经验分享
- C++动态分配内存的字符串
- C++中导致程序效率变低的一些细节
- C++传递函数指针
- C++ Primer 学习笔记_2_高速入口(继续)
- C++操作word:插入文字、图片、表格,设置样式字体
- C语言字节对齐
- C++教材
- 关于C++ cin的问题
- CPP-Templates
- C++primer_拷贝控制之13.26联系控制成员实现类值行为
- C++双向循环链表实现
- c++ 动态规划(重构解).
- c++引入依赖 include (转)
- 黑马程序员--C语言自学笔记---03运算符
- c++ 浅复制与深复制
- C++实现简单的双向链表
- 单链表的操作_二分查找