红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【C++】右值引用与移动语义

右值引用

左值与右值

左值:能用在赋值运算符左侧的表达式

右值:能用在赋值运算符右侧,但不能用在左侧的表达式

1
2
int a = 0;
a = 5;

这里a为左值,5为右值,不能写成5=a

判断方法:

左值:能够获得某个表达式的引用或地址即为左值

1
2
3
4
5
6
7
const int a = 5;
const int& b = a;

int c;
int* p = &c;

int b = &4
  • b取到了a的引用,a是(不可修改的)左值
  • p取到了c的地址,c是左值
  • 4不能取地址,4是右值

C++11后的分类

  • glvalue:泛左值
  • prvalue:纯右值
  • xvalue:将亡值

举例

  • 左值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 整型/浮点型等变量
int a = 8;
double m = 1.5;
int*p = new int{5}; // 指针
*p = 4; // 表达式解引用
int b[5]; 
b[1] = 10; // 数组(不可更改)、数组元素

int& r = a; // 左值引用中右侧的表达式

// 类的数据成员
struct s{
    int id;
}s1;
s1.id = 3; 

// 返回引用的函数的调用表达式
int& refn(){
    static int n = 1;
    return n;
}
refn()=5;
  • 纯右值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int x,y,z;
int m = 3, n = 1;
double d;

x = 2; // 字面值
// 将计算结果存放在临时中间对象中的表达式
x = m+n;
y = -m;
z = n+2;

d = double(z) // 类型转换
    
struct s{};
s obj;
obj = s{}; // 未命名类的对象

s func(){
    return s{};
}
func(); // 函数调用返回对象值时的调用
s (*f)(int) = &func; // 函数地址

// 非静态类成员函数、枚举、数组、this指针、lambda表达式、一些内置运算符表达式

左右值引用

  • 左值引用:普通的引用
1
2
int a = 5;
int& b = a;
  • 右值引用:使用两个&进行引用
1
int &&c = 5;

只能绑定右值,不能绑定左值

应用

函数重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
void func(int& a){
  cout <<"调用左值引用重载"<<endl;
}
void func(int&& a){
  cout <<"调用右值引用重载"<<endl;
}

int main(){
  int a = 5;
  func(a);
  func(5);
}
1
2
调用左值引用重载
调用右值引用重载

移动语义

移动构造函数

当类中存在指针等变量,而没有设置拷贝构造,编译器会自动生成拷贝构造函数,此时为浅拷贝。我们应该自己设计深拷贝,申请新的内存,并将原来的内容复制到新分配的内存中,并实现赋值运算符的重载函数,释放原有内存,申请新的内存,并复制值

此时参数中为左值引用,称为拷贝赋值运算符重载函数

为了实现移动语义,我们需要实现移动赋值运算符重载函数

此时参数为右值引用,且没有const限定符

该方法少了内存的分配和数据的复制,修改了被移动对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
using namespace std;
class CharBuffer{
public:
// 左值函数
  CharBuffer(int size):m_buff(new char[size]), m_size(size){
    cout <<"普通构造函数"<<endl;
  }
  CharBuffer(const CharBuffer& other):m_size(other.m_size), m_buff(new char[m_size]){
    memcpy(m_buff, other.m_buff, m_size);
    cout<<"拷贝构造函数"<<endl;
  }
  CharBuffer& operator=(const CharBuffer& other){
    if (this==&other){
      return *this;
    }
    m_size = other.m_size;
    delete[] m_buff;
    m_buff = new char[m_size];
    memcpy(m_buff, other.m_buff, m_size);
    cout <<"拷贝赋值运算符"<<endl;
  }
// 右值函数
  CharBuffer(CharBuffer&& other):m_size(other.m_size), m_buff(other.m_buff){
    other.m_buff = nullptr;
    other.m_size = 0;
    cout <<"移动构造函数"<<endl;
  }
  CharBuffer& operator=(CharBuffer&& other){
    if (this==&other){
      return *this;
    }
    delete[] m_buff;
    m_size = other.m_size;
    m_buff = other.m_buff;

    other.m_size= 0;
    other.m_buff = nullptr;
    cout <<"移动赋值运算符"<<endl;
  }
  int m_size;
  char* m_buff;

  ~CharBuffer(){
    delete[] m_buff;
    cout <<"析构函数"<<endl;
  }
};

int main(){
  CharBuffer buff2{CharBuffer(100)};
}

拷贝省略

当我们运行语句,构造一个未命名的对象作为右值

CharBuffer buff2{CharBuffer(100)};

按照移动语义,会先通过普通构造函数创建临时对象,然后将该临时对象赋值给buff2,但编译器会对这种代码进行优化,省略调临时对象的创建,直接在目标存储位置构造该对象即输出:

1
2
普通构造函数
析构函数

C++17后,这种优化成为语言规范

返回值优化(RVO)

类似,函数直接返回值的情况下,临时对象的创建也会省略

1
2
3
4
5
6
7
CharBuffer generate(int n){
  return CharBuffer(n);
}

int main(){
  CharBuffer buff2 = generate(100);
}
1
2
普通构造函数
析构函数

具名的返回值优化(NRVO)

1
2
3
4
5
6
7
8
9
10
CharBuffer generate_nv(int n){
  CharBuffer buf(n);
  cout <<&buf<<endl;
  return buf;
}

int main(){
  CharBuffer buff2 = generate_nv(100);
  cout <<&buff2<<endl;
}
1
2
3
4
普通构造函数
0x61fe30
0x61fe30
析构函数

buff2与buff的地址相同,说明他们是对同一个对象的引用,原理同上

std::move()

该函数将传入的参数转换为右值引用并返回

1
2
3
4
int main(){
  CharBuffer buff1(100);
  CharBuffer buff2(move(buff1));
}
1
2
3
4
普通构造函数
移动构造函数
析构函数
析构函数

此时由于传入的转换为了右值引用,故正确调用了移动构造函数

等同于static_cast<CharBuffer&&>(buff1)

xvalue

1
2
3
4
5
6
7
int main(){
  CharBuffer buff2(10);
  {
    CharBuffer buff1(100);
    buff2 = move(buff1);
  }
}

这段代码中,buff1就是一个将亡值,我们使用move函数获得其右值引用,从而将它的资源回收再利用,转移buff2中

常见xvalue

  • 函数返回的右值引用
  • static_cast<T&&>(v)
  • 未命名的右值对象获得的非静态成员变量

移动语义的应用

大部分情况下都不需要移动语义

  • 若一个类涉及到深拷贝,需要复制较多数据,可以使用移动语义
  • 当转移unique_ptr的所有权时,需要移动语义
  • 编写供其他程序使用的STL,需要提供移动语义的支持