0%

左右值引用

1. 左值、右值

左值:

  • 表达式结束后依然存在的持久性对象或者函数,可以被取地址。
  • 左值是可以被标识符引用的表达式,它们代表着内存中的一个位置。例如,变量、数组元素、通过引用或指针访问的对象、具有名称的对象等都是左值。
  • 左值在赋值操作符左侧出现,可以接受赋值操作。

右值:

  • 表达式结束后就消失的临时对象,通常不能被取地址。
  • 右值是不具有持久性的临时对象或者表达式的结果,不能被直接引用。例如,字面常量、临时对象、未命名的临时对象、返回临时对象的函数调用等都是右值。
  • 右值通常出现在赋值操作符右侧,用于提供值给赋值操作。

2. 左值引用、右值引用

左值引用

  • 左值引用是通过使用 & 符号声明的引用类型。它绑定到左值,并且可以延长左值的生命周期,使得左值可以在函数调用等情况下被修改。
  • 左值引用不能绑定到右值。
  • 左值引用常用于函数参数传递、函数返回值和操作符重载等场景,可以实现有效的引用语义和避免不必要的拷贝。
1
2
3
4
5
int a = 5;
int &b = a; // b是左值引用
b = 4;
int &c = 10; // error,10无法取地址,无法进行引用
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址

可以得出结论:对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。

右值引用

  • 右值引用是通过使用 && 符号声明的引用类型。它主要用于绑定到临时对象或者即将销毁的对象,允许移动语义的使用。
  • 右值引用可以绑定到右值,但不能绑定到左值。
  • 右值引用的引入使得可以实现移动语义,即将资源从一个对象“移动”到另一个对象,而不是进行昂贵的拷贝操作。这在处理临时对象时尤为有用,能够提高性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
public:
A() {} // 默认构造函数
A(const A& other) {} // 拷贝构造函数
A(A&& other) {} // 移动构造函数
};

A createA() {
return A(); // 返回一个临时对象
}

int main() {
A a1 = createA(); // 调用移动构造函数,避免了拷贝
return 0;
}

std::move函数:把一个左值强制转换成一个右值 。

1
2
3
int a = 4;
int &&b = a; // error, a是左值
int &&c = std::move(a); // ok

3. 移动语义

  • 利用右值引用允许在对象间转移资源的所有权,而不是传统的拷贝。
  • 这种转移资源的操作比拷贝操作更高效,特别是对于动态分配的内存或者其他资源。
  • 移动语义的引入是为了解决传统的拷贝操作在处理临时对象时可能造成性能开销的问题。
  • 移动语义的核心概念在于,当一个对象被标记为将要被销毁(右值),它的资源可以被安全地“窃取”到另一个对象,而不是简单地复制。这意味着移动操作将对象的资源所有权从一个对象转移到另一个对象,而不会发生资源的复制或者额外的分配。

作用

  1. 避免不必要的拷贝操作
  2. 实现资源管理的高效转移
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Resource {
public:
Resource() {}
// 移动构造函数
Resource(Resource&& other) {
// 窃取资源
this->data = other.data;
other.data = nullptr; // 将原始对象置为空,避免资源重复释放
}

private:
int* data;
};

int main() {
Resource res1;
Resource res2 = std::move(res1); // 调用移动构造函数
return 0;
}

4. 完美转发

  • 允许将参数传递到另一个函数,并保留原始参数的值类别(左值或右值)和const修饰符。
  • 目标是在保留参数类型的同时,将参数转发给其他函数,实现通用性更强的函数包装或者委托。
  • 完美转发的关键是使用了右值引用模板参数推导。通过使用 std::forward 函数模板,可以在传递参数时保留参数的值类别,确保参数被按原样传递。这种机制在实现泛型编程时尤为有用,可以减少代码的重复和增加可读性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
void wrapper(T&& arg) {
// arg 是右值引用或者左值引用,依据传递的实参类型决定
// 将参数 arg 转发给另一个函数 foo()
foo(std::forward<T>(arg)); // 完美转发
}

void foo(int& x) {
cout << "left value" <<endl;
}
void foo(int&& x) {
cout << "right value" <<endl;
}

int main() {
int x = 42;
wrapper(x); // 传递左值
wrapper(42); // 传递右值
return 0;
}

wrapper 函数模板通过 std::forward 将参数 arg 完美转发给 foo 函数。无论 arg 是左值还是右值,都可以在 foo 中保持其原始的属性,并且正确地调用对应的 foo 函数重载。

运行结果:

1
2
42: left value
42: right value