右值引用,std::move(移动语义)和std::forward(完美转发)都是C++11里面的特性。
使用右值引用和移动语义,可以避免无谓的复制,提供了程序性能。
在说明右值引用之前,先说下什么是左值,什么是右值。 左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
区分左值和右值的便捷方法是看能不能对表达式取地址,如果能则为左值,否则为右值;
将亡值是C++11新增的、与右值引用相关的表达式,比如:将要被移动的对象、T&&函数返回的值、std::move返回值和转换成T&&的类型的转换函数返回值。
C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值。
右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内
存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
在这里先提一下,具有T&&val,不一定是右值引用,也可能是左值引用,具体要看val是什么值,如果val是一个左值,那么就是一个左值引用,如果是一个右值,那就是一个右值引用。这点非常让人迷糊,后面使用代码在进行说明,这里先记一下
我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
下面以类的拷贝构造和移动构造函数来说明右值引用和std::move的作用
#include
#include
using namespace std;
class MyString {
public:
MyString() {
std::cout << "无参构造函数" << std::endl;
}
MyString(const string& ptr) :m_ptr(new string(ptr)) {
std::cout << "有参构造函数" << std::endl;
}
//由于是指针,拷贝的时候需要做深拷贝,如果做浅拷贝的话,会释放2次,会导致异常发生
MyString(const MyString& m_str) {
if (m_str.m_ptr) {//做一个非空判断,保证安全
m_ptr = new string(*m_str.m_ptr);
}
std::cout << "拷贝构造函数" << std::endl;
}
MyString& operator=(const MyString& other) {
if (&other != this)//如果不是自生,做赋值操作
{
if (m_ptr) { //防止赋值之前已经存在资源,如果不释放就会造成内存泄漏
delete m_ptr;
std::cout << "copy operator= delete" << std::endl;
}
m_ptr = nullptr;
if (other.m_ptr) //如果传递的为nullptr,
{
m_ptr = new string(*other.m_ptr);//也可能出现问题,这里不考虑,假如new失败了,m_ptr就不能返回之前的状态了。
}
std::cout << "拷贝赋值函数" << std::endl;
}
return *this;
}
//与拷贝构造的区别,参数不能使用const,因为参数会进行移动
//使用的是浅拷贝,直接把之前的资源拿过来,而不是去做真正的内存开辟操作
//使用 noexcept 修饰 是为了提供强异常保证,
//即在移动过程中即使发生异常(资源也能够恢复为之前的状态),也能保证程序的正确性。
MyString(MyString&& m_str) noexcept :m_ptr(m_str.m_ptr) {
m_str.m_ptr = nullptr;//移动资源过后,设置为nullptr,不然会导致多次调用析构的delete
std::cout << "移动构造函数" << std::endl;
}
MyString& operator=(MyString&& other) noexcept {
if (&other != this)//如果不是自己,做赋值操作
{
if (m_ptr) { //防止赋值之前已经存在资源,如果不释放就会造成内存泄漏
delete m_ptr;
std::cout << "move operator= delete" << std::endl;
}
m_ptr = other.m_ptr;
other.m_ptr = nullptr;//移动资源过后,设置为nullptr,不然会导致多次调用析构的delete
std::cout << "移动赋值函数" << std::endl;
}
return *this;
}
~MyString() {
if (m_ptr)
{
std::cout << "delete ptr:" << m_ptr << std::endl;
delete m_ptr;
}
m_ptr = nullptr;
std::cout << "~Mystring" << std::endl;
}
private:
std::string* m_ptr = nullptr;//提前设置为nullptr,防止指针生成一个随机值,释放一个无效指针报错
};
int main{
{
MyString a,b;//调用2次无参构造函数
//先调用有参构造构造出MyString("Hello")临时对象--->会分配一次内存
//由于MyString("Hello") 返回的对象是一个将亡值(右值),随后通过这个右值赋值给a后调用移动赋值
//赋值完成之后,MyString("Hello")临时对象生命周期也结束了,自然会调用析构函数,
//但是里面的new出来的指针对象会移动到a对象中,因此指针的内容是不会被释放的。
a = MyString("Hello");//有参构造->移动赋值->析构函数(不会释放资源)
//由于a是一个左值,不会有任何释放操作,仅仅调用拷贝构造函数(会分配内存)a和d都需要释放内存
MyString d = a;//拷贝构造
b = std::move(a);//移动赋值
b = a; //拷贝赋值函数
}
在上面的代码里,构造了一个MyString类,里面有一个string类型的指针,同时这个类具有构造,析构,拷贝构造,拷贝赋值,移动构造,移动赋值函数。
在使用的时候,如果使用了移动赋值和移动构造,不会new对象,生成一个新的对象,而是把别人的资源给抢占过来,供自己使用,但是如果使用拷贝构造和拷贝赋值的时候,会为string类型的指针开辟一个空间,然后将内容复制过来。也就是深拷贝,在使用std::move的时候可以将左值a变为右值,从而节约了拷贝的开销,这在类中具有大数据结构的时候是非常有必要的。因此右值和move语义在开发中是不可缺少的
在说明完美转发之前,我们先用一段测试代码来说明其作用。
#include
template<class T>
void print(T& t)
{
std::cout << "L:" << t << std::endl;
}
template<class T>
void print(T&& t)
{
std::cout << "R:" << t << std::endl;
}
///
/// 注意,&&引用类型即可以时左值引用,也可以是右值引用
/// 引用之后,val值就变成了一个左值(需要注意)
///
///
///
template<class T>
void func(T&& val)
{
print(val);//左
print(std::move(val));//右
print(std::forward<T>(val));//保持val的左值或者右值属性
}
int main {
//a是一个右值引用,但其本身a也有内存名字,所以a变成了一个左值
int&& a = 10;//虽然使用了&&,不要误认为a就是右值了,a这时候是一个左值了
//int&& b = a;//由于a是左值,不能使用右值引用去引用左值了。
int& c = a;//a是左值,可以使用左值引用去引用。
std::cout << "func(1)" << std::endl;
func(1);//1是右值,执行fun过后之后,变成:左,右,右
int x = 10;
int y = 20;
std::cout << "func(x)" << std::endl;
func(x);//x是左值,执行fun过后之后,变成:左,右,左
std::cout << "func(std::forwardy)" << std::endl;
func(std::forward<int>(y));//std::forward(y)会将左值转换为右值,执行fun过后之后,变成:左,右,右
}
当传递右值1的时候,func(1)的输出为左,右,右,这就很奇怪了,明明传递了一个右值 ,为啥经过模板函数template func(T && val),val就变成了左值?,是否有什么办法完美转发这个属性呢?
对于下面这个模板函数
Template<class T> void func(T &&val);
前面有提到过,对于 &&类型,既可以是一个左值引用,也可以是一个右值引用,具体要看val是什么值,在这里val其实是一个左值,而不是一个右值,即使我们传递了一个1。 对于下面代码:
int &&a = 10;
int &&b = a; //错误,a是一个左值,不能用一个右值引用去存储
a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这
是不对的。
那么有什么办法可以让这个1识别为一个右值呢,那就是使用std::forward完美转发,调用func(1),然后在调用print(std::forward(val)),就知道std::forward完美转发了其属性(如果传递的是右值,那么就会保持右值得属性,如果传递得是左值,就会保持左值的属性)。
在main函数里func(std::forward(y));这里y是一个左值,但是使用std::forward(y)变成了右值,这里需要注意以下,这时候std::forward和std::move一样了。
右值引用和move语义结合起来,能够很好的实现移动语义,具体的move语义将一个左值变成一个右值,右值引用实现了移动语义,完美转发std::forward在函数调用时,能够保持变量的本来属性。但是std::forward在同一个函数调用时,会将左值变成一个右值,这跟std::move是一样的。