右值引用和移动构造 (C++11)

在C++中,左值(lvalue)右值(rvalue) 是两个重要的概念,它们分别指代表达式在内存中的行为方式。同时,由右值引出了一系列的概念都是C++11中比较重要的知识点,这篇文章我们一起来学习一下吧~

1. 左值和右值的区分

左值(lvalue)

  • 定义: 左值是指一个具有持久地址的对象,能够被赋值被取地址操作符&所操作。它通常代表内存中的一个具体位置。
  • 例子:
    • 变量名: int x = 10; 中的 x 是一个左值。
    • 数组元素: int arr[5]; arr[2] = 3; 中的 arr[2] 是一个左值。
    • 解引用: int* p = &x; *p = 20; 中的 *p 是一个左值,动态开辟的空间也是可以的int* p = new int

右值(rvalue)

  • 定义: 右值是指不具持久地址、仅存在于表达式计算中的值。它们通常是临时值(将亡值)或字面常量,不适合被赋值操作。
  • 例子:
    • 字面值: int y = 42; 中的 42 是一个右值。
    • 表达式结果: int z = x + y; 中的 x + y 的结果是一个右值。
    • 临时对象: std::string("Hello") 创建的临时字符串对象是一个右值。

简单区分方法

  • 左值: 可以出现在赋值操作符的左边。
  • 右值: 只能出现在赋值操作符的右边。

实例解析

int a = 5;     // a 是左值, 5 是右值
int b = a + 10; // a + 10 是右值
int* p = &a;   // &a 是左值

现代C++中与右值相关的概念

根据左右值之间的区别,现代C++中引入了 右值引用(rvalue reference)移动语义(move semantics) 等特性,这些特性允许开发者更高效地管理资源,例如通过使用std::move来避免不必要的拷贝。这个就是后面要展开讲解的内容。

2. std::move的用法

std::move是C++标准库中的一个函数模板,它的主要作用是将一个左值显式地转换为右值,从而可以触发右值引用的绑定。这在实现移动语义避免不必要的拷贝时非常有用。

主要用途(这部分需要结合后面的知识来看)

  1. 触发移动构造函数或移动赋值运算符:

    • 当你将一个对象传递给函数或者返回一个对象时,如果你不使用std::move,该对象会以左值的方式传递,通常会调用拷贝构造函数或拷贝赋值运算符。
    • 通过使用std::move,你可以将对象转换为右值引用,从而触发移动构造函数或移动赋值运算符,以便直接转移资源而不是复制资源。
  2. 避免不必要的拷贝:

    • std::move可以减少大对象的拷贝次数,这对于性能敏感的应用程序(如处理大量数据的程序)非常重要。

3. 左值引用和右值引用

左值引用(lvalue reference)

  • 定义: 左值引用是指向左值的引用,可以通过引用来修改被引用的对象。左值引用的类型声明使用单个&符号。我们过往学的都是左值引用,这里不做过多的讲解,详细内容可看这篇:链接(C++基础知识)

右值引用(rvalue reference)

  • 定义: 右值引用是指向右值的引用,不需要额外的存储空间或创建新的变量,主要用于优化移动语义,以避免不必要的对象拷贝。右值引用的类型声明使用双&&符号。

小栗子

// 以下几个都是对右值的右值引用
int&& rr1 = 10;				// 常量
double&& rr2 = x + y;		// 表达式
double&& rr3 = fmin(x, y);  // 函数返回值(除左值引用返回)

// const左值引用既可引用左值,也可引用右值。
// 因为加了const可以保证原来的右值不被修改
const int& ra3 = 10;
int a = 10;
const int& ra4 = a;

// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;

注意:右值引用本身是左值,因为右值引用可以取地址,设置右值引用就是为了转移资源,修改其内容,所以为左值,而如果为右值的话无法起到这个作用。

左值引用的弊端

  • 左值引用做参数,可以完全避免传参时不必要的拷贝操作。
  • 左值引用做返回值,并不能完全避免函数返回对象时不必要的拷贝操作,原因如下:

如果函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的弊端所在。而这种情况就是右值引用发挥作用的时候了。

右值引用的典型用法:【移动语义】

右值引用的一个典型应用是实现移动语义,这在处理临时对象时特别有用。例如,当你想避免对象拷贝时,可以使用右值引用进行移动操作,这样可以显著提高性能,特别是在处理大对象时。

#include <iostream>
#include <string>

class MyClass {
public:
    std::string data;

    MyClass(std::string str) : data(std::move(str)) {}
	// 移动构造:传递右值的构造函数
    MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {}  
};

int main() {
    MyClass obj1("Hello");
    MyClass obj2(std::move(obj1)); // obj2 使用右值引用来接收 obj1 的资源,调用移动构造
    std::cout << "obj2 data: " << obj2.data << std::endl;
    return 0;
}

情况分析

  • 资源转移: std::move(obj1)obj1转换为右值引用,然后将它传递给obj2的移动构造函数。在移动构造函数中,obj1data成员的内容被转移到obj2,因此obj1.data的内容将被清空或置于某种未定义但有效的状态(例如,空字符串或空容器)。

  • 状态:
    obj1本身仍然是一个有效的对象,但其内部资源可能已经被转移,obj1.data通常会变成一个空字符串或空容器。

上面效率的提升,针对的自定义类型的深拷贝的类,因为深拷贝的类才有资源转移的移动系列函数;对于内置类型和浅拷贝自定义类型,没有移动系列函数。


4. 完美转发

万能引用

template<class T>
void PerfectForward(T&& t)
{
	//...
}

在模板中&&表示万能引用,既能接受左值,又能接受右值。这一概念在C++11引入模板类型推导自动类型推导(auto) 的上下文中非常重要。

如何识别万能引用

万能引用有两个条件:

  1. 使用T&&的形式,其中T是一个模板参数(或通过auto推导出的类型)。
  2. 使用上下文中,T可以绑定到左值或右值。

典型示例

template<typename T>
void func(T&& param) {
    // param 是一个万能引用
}

int main() {
    int x = 10;
    func(x);          // 传入左值,T 被推导为 int&,param 是 int&
    func(20);         // 传入右值,T 被推导为 int,param 是 int&&
}

万能引用的用途

完美转发(Perfect Forwarding):

  • 万能引用在模板函数中广泛应用于实现完美转发,即函数能够保持传入参数的左值或右值属性并将其传递给另一个函数。完美转发是编写高效泛型代码的关键技术。
  • 例子:
    template<typename T>
    void wrapper(T&& arg) {
        func(std::forward<T>(arg));  // 完美转发 arg
    }
    

解释:因为右值引用本身是左值,所以在一次参数传递中右值退化成了左值,如果在此过程中想要保持右值属性,就需要用到完美转发

例子:完美转发

#include <iostream>
#include <utility>

void process(int& x) {
    std::cout << "Processing lvalue" << std::endl;
}

void process(int&& x) {
    std::cout << "Processing rvalue" << std::endl;
}

template<typename T>
void forwarder(T&& arg) {
    process(std::forward<T>(arg));  // 完美转发
}

int main() {
    int a = 10;
    forwarder(a);         // 传入左值,调用左值版本的 process
    forwarder(20);        // 传入右值,调用右值版本的 process
}
  • 解释
    • forwarder函数中的T&& arg是一个万能引用,它可以绑定到左值或右值。
    • std::forward<T>(arg)会根据T的类型保持arg的左值或右值属性,并将其传递给process函数。

总结:右值引用、移动构造是C++11中比较好用的提高效率的方式,在STL库中也有很多的应用,包括emplace_back接口、移动赋值等等,在后面的文章中会有提及,大家可以期待一下。

如果你能看到这里,给你点个赞,如果对你有帮助的话不妨点赞支持一下~

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值