右值引用与移动语义

简介: 右值引用与移动语义

一、左、右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以我们称之前学习的引用为左值引用。但无论左值引用还是右值引用,其实都是给对象取别名。


1.1 什么是左值

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,也可以对它赋

值。左值可以出现赋值符号的左边,但右值不能出现在赋值符号左边。定义时const修饰符后的左

值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。(可以大致理解为,能够取地址的一般都为左值)

int main()
{
    //以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    //以下是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;
    return 0;
}

1.2 什么是右值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(非左值引用返回)等等。右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。


内置类型的右值引用被称为纯右值,自定义类型的右值引用被称为将亡值


int main()
{
    double x = 1.1, y = 2.2;
    //以下是常见的右值
    10;
    x + y;
    fmin(x, y);
    //以下是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    return 0;
}

1.3 右值引用特性

右值是不能取地址的,但给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。(即可以给右值引用取地址)

因为右值引用是左值

int main()
{
    //不能取字面量10的地址。
    //但是rr引用后,可以对rr取地址,也可以修改rr。
    int&& rr = 10;
    rr = 20;
    return 0;
}

1.4 move语义

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?


有些场景下,可能真的需要用右值引用去引用左值实现移动语义。当需要用右值引用去引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于<utility>头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值,然后实现移动语义。


a5ebb5a0bfbe43b8b62b674db4c70a32.png


template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
    // forward _Arg as movable
    return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
    bjy::string s1("hello world");
    bjy::string s2(s1);//这里s1是左值,调用的是拷贝构造
    bjy::string s3(std::move(s1));//将s1 move处理以后,会被当成右值,调用移动构造
    //一般是不这样用的,因为我们会发现s1的资源被转移给了s3,s1被置空了。
    return 0;
}

二、左、右值引用的比较

左值引用:

1. 左值引用只能引用左值,不能引用右值

2. const左值引用既可以引用左值,也可以引用右值

int main()
{
    //左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;//ra为a的别名
    //int& ra2 = 10;//编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用:

1. 右值引用只能引用右值,不能引用左值

2. 右值引用可以move以后的左值

int main()
{
    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10;
    int a = 10;
    int&& r2 = a;
    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    // message : 无法将左值绑定到右值引用
    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a);
    return 0;
}


三、右值引用的使用场景

const左值引用既可以引用左值,也可以引用右值,那么为什么还需要右值引用呢?


af271447123e48a5b57b633b9e959834.png


左值引用看似功能已经很完善了,但是在面对下面这些情况时,却捉襟见肘。


3.1 左值引用的短板

当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。


ff2d355d82834313ac3b5421520cbc2f.png


3.2 解决方案

这个时候右值引用便可以解决这个问题了。

c3c4f687f6b14d47be7c04be62253566.png

// 拷贝构造
string(const string& s) :_str(nullptr), _size(0), _capacity(0)
{
  cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
  string tmp(s._str);
  swap(tmp);
}
// 移动构造
string(string&& s) :_str(nullptr), _size(0), _capacity(0)
{
  cout << "string(string&& s) -- 资源转移" << endl;
  swap(s);
}

利用右值引用提供了移动构造函数后,to_string函数中的str对象会被编译器识别为将亡值。之后需要发生构造时,则会自动调用移动构造函数。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造。就是窃取别人的资源来构造自己,移动构造中没有新开空间,拷贝数据,所以比拷贝构造更加高效。


与移动构造类似的还有移动赋值,也是通过右值引用来提高效率。

// 拷贝赋值
string& operator=(const string& s)
{
  cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
  string tmp(s);
  swap(tmp);
  return *this;
}
// 移动赋值
string& operator=(string&& s)
{
  cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
  swap(s);
  return *this;
}


bff4762db80f4e29b826ebe57ebc6885.png


四、移动构造与移动赋值

那么移动构造和移动赋值有什么需要注意的地方吗?


在C++98时,我们学习过C++的类中一共有6个默认成员函数(分别是构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载)。但随着C++11的更新又新增了两个默认成员函数,即移动构造函数和移动赋值重载。


注意情况

1. 若没有自主实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员则需要看这个成员是否存在移动构造,若存在就调用移动构造,不存在就调用拷贝构造。


2. 若没有自主实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员则需要看这个成员是否存在移动赋值,若存在就调用移动赋值,不存在就调用拷贝赋值。


3. 若提供了移动构造或者移动赋值中任意一个,编译器不会自动提供拷贝构造和拷贝赋值。


五、万能引用与完美转发

5.1 万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,但是引用类型就会被限制,在后续使用中都退化成了左值。所以万能引用也被称为引用折叠(即左值引用和右值引用都被折叠为左值)。

也可以换一种理解方式。在前面提到过右值引用的特性,右值引用是左值,且左值引用也是左值。所以不出意外,既能接收左值也能接收右值的万能引用也是左值。

#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}
int main()
{
    PerfectForward(10); //左值引用
    int a;
    PerfectForward(a); //左值引用
    PerfectForward(std::move(a)); //左值引用
    const int b = 8;
    PerfectForward(b); //const 左值引用
    PerfectForward(std::move(b)); //const 左值引用
    return 0;
}

如果希望能够在传递过程中保持它的左值或者右值的属性, 就需要用到完美转发


5.2 完美转发

std::forward 完美转发在传参的过程中保留对象原生类型属性

#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
   Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); //右值引用
    int a;
    PerfectForward(a); //左值引用
    PerfectForward(std::move(a)); //右值引用
    const int b = 8;
    PerfectForward(b); //const 左值引用
    PerfectForward(std::move(b)); //const 右值引用
    return 0;
}


使用场景

在实际开发中,某些接口函数是提供了右值引用版本的,譬如STL中vector、list等容器的插入接口。传入右值参数并被右值引用接收后,会被认为是左值,无法顺利调用到移动构造和移动赋值等函数(没有真正减少拷贝、提高效率),这时就需要使用完美转发来在传参过程中保证右值对象的属性。


目录
相关文章
|
11月前
|
C++
C++11 右值引用和移动语义(三)
C++11 右值引用和移动语义
37 0
|
3月前
|
编译器
右值引用和移动语义
右值引用和移动语义
|
4月前
|
存储 安全 程序员
C++11:右值引用
C++11:右值引用
27 0
|
11月前
|
编译器 C++ 容器
C++11 右值引用和移动语义(二)
C++11 右值引用和移动语义
31 0
|
4月前
|
编译器 C++
c++左值、右值引用和移动语义
c++左值、右值引用和移动语义
41 0
|
4月前
|
安全 编译器 C++
|
11月前
|
C++
C++11 右值引用和移动语义(一)
C++11 右值引用和移动语义
26 1
|
11月前
|
存储 编译器 C语言
C++11:右值引用和移动语义
C++11:右值引用和移动语义
44 1
|
4月前
|
编译器 C++
深入理解 C++ 右值引用和移动语义:全面解析
C++11引入了右值引用,它也是C++11最重要的新特性之一。原因在于它解决了C++的一大历史遗留问题,即消除了很多场景下的不必要的额外开销。即使你的代码中并不直接使用右值引用,也可以通过标准库,间接地从这一特性中收益。为了更好地理解该特性带来的优化,以及帮助我们实现更高效的程序,我们有必要了解一下有关右值引用的意义。
70 0
|
编译器 C++
C++10中的移动语义
对象的移动语义(Move Semantics)需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。如果源对象是在复制或者赋值结束以后被销毁的临时对象,编译器会使用两种方法。移动构造函数和移动赋值运算符将成员变量从源对象复制/移动到新对象,然后将源对象的变量设置为空值。这样做实际上将内存的所有权从一个对象转移到另一个对象。这两种方法基本上只对成员变量进行浅拷贝(shallow copy),然后转换已分配内存的权限,从而防止悬挂指针和内存泄露。
63 0
C++10中的移动语义