右值引用:移动语义和完美转发
左值和右值
区分左值、右值
等号左边的一定是左值,左值可以被赋值,左值可以赋值给其它对象。
等号右边的不一定是右值。右值只能出现在等号右边,其它对象不能赋值给具有右值属性的对象。
右值概念
右值分为纯右值和将亡值,右值对象不能取地址。
纯右值主要识别临时变量和一些不跟对象关联的值。比如:常量、一些运算表达式(1+3)、lamda表达式等。
将亡值声明周期将要结束的对象。比如:在函数值返回时的临时对象。
左值引用
函数传参或者返回值时候减少不必要的拷贝,比如swap场景。
void myswap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
右值引用
右值引用书写格式:类型&& 引用变量名字 = 实体;
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class MyString{
public:
MyString(char* ptr="")
{
_str = new char[sizeof(ptr)+1];
strcpy(_str, ptr);
}
~MyString(){}
public:
char* _str;
};
MyString GetStr(char* ptr)//char to MyString
{
MyString Temp(ptr);
return Temp;
}
int main()
{
MyString&& s=GetStr("Hello World!!!");
cout << s._str << endl;
system("pause");
return 0;
}
上述的GetStr函数返回值是一个临时对象,临时对象不能在函数体外使用。临时对象是将亡值,属于右值,意味着,它的声明周期即将结束,那我可以定义一个右值引用s来将这个将亡值的内存继承。
右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
移动构造
我们可以定义一个右值引用将一个右值(将亡值)的内存资源获取。那么在拷贝构建一个对象时候,如果传入参数是一个右值,那么我们就可以直接引用这个右值,无需开辟资源深拷贝。这种做法称之为移动构造。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class MyString{
public:
MyString(char* ptr="")
{
_str = new char[sizeof(ptr)+1];
strcpy(_str, ptr);
}
MyString(MyString& s2)//普通拷贝构造,深拷贝
{
_str = new char[sizeof(s2._str) + 1];
strcpy(_str, s2._str);
}
//移动构造,当拷贝构造函数参数为右值时候,意味着传入参数的内存资源可以被继承
MyString(MyString&& s2)
:_str(s2._str){}
~MyString(){}
public:
char* _str;
};
MyString GetStr(char* ptr)//char to MyString
{
MyString Temp(ptr);
return Temp;
}
int main()
{
MyString s1("Hello ");
//s2会调用移动构造函数,因为移动构造不需要开辟内存资源,系统会优先调用
MyString s2(GetStr("World"));
cout << s1._str << s2._str << endl;
system("pause");
return 0;
}
std::move
功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class MyString{
public:
MyString(char* ptr = "")
{
_str = new char[sizeof(ptr)+1];
strcpy(_str, ptr);
}
~MyString(){}
public:
char* _str;
};
int main()
{
MyString s1("Hello World!!!");//左值s1
MyString&& s2 = std::move(s1);//右值s2
s1 = MyString("你好,世界!!!");//s1仍然是左值
cout << s1._str << " " << s2._str << endl;
system("pause");
return 0;
}
这里的s2并没有自己独立内存,它是s1的右值引用,也就是s1的别名。所以输出结果都是你好,世界!!
const左值引用
常量左值引用就是个"万能"的引用类型.可以接受左值和右值对其进行初始化。而且使用右值对其初始化的时候,常量左值引用还可以向右值引用一样将右值的生命周期延长.不过相比于右值引用所引用的右值,常量左值引用所引用的右值在它的"余生"中只能是只读的。 相对的,非常量左值引用只能接受非常量左值对其初始化。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class MyString{
public:
MyString(char* ptr="")
{
_str = new char[sizeof(ptr)+1];
strcpy(_str, ptr);
}
~MyString(){}
public:
char* _str;
};
MyString GetStr(char* ptr)//char to MyString
{
MyString Temp(ptr);
return Temp;
}
int main()
{
const MyString& s1=GetStr("Hello World");//常量左值引用将亡值(右值)
cout << s1._str << endl;
system("pause");
return 0;
}
完美转发
函数参数是右值时候,调用右值引用的函数时候,函数参数会丢失其右值属性。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int Func(int&& a)
{
a = 20;//丢失了右值属性的右值引用
return a;
}
int main()
{
cout<<Func(10)<<endl;//纯右值
system("pause");
}
这并不完美,我们需要函数参数在传参之后任然保持其原来的属性才完美。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。 这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
void Fun(int &&x)
{
cout << "rvalue ref" << endl;
}
void Fun(const int &x)
{
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)
{
cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}