C++11之右值引用与移动语义(提高效率)重要

发布于:2025-07-22 ⋅ 阅读:(12) ⋅ 点赞:(0)


在这里插入图片描述

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
在C++11之前,C++中只有左值引用(&)。左值引用主要用于绑定到已命名的变量,用于函数参数传递、返回值等场景,但无法直接绑定到临时对象。临时对象通常是匿名的,它们在表达式求值过程中产生,并在表达式结束后销毁。例如,std::string(“temporary”)就是一个临时对象。
然而,临时对象在很多场景下都存在资源浪费的问题。以std::vector为例,当我们对一个std::vector对象进行拷贝赋值时,即使源对象是一个临时对象,目标对象也会先分配一块新的内存,然后将源对象中的元素逐个拷贝过去,最后销毁临时对象。这个过程不仅涉及额外的内存分配和拷贝操作,还可能导致不必要的性能开销。
为了解决这一问题,C++11引入了右值引用(&&)。右值引用可以绑定到临时对象,从而允许我们直接操作临时对象的资源,避免不必要的资源浪费。

一.左值和右值

左值的定义

• 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

右值的定义

• 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

• 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loacto rvalue的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址

二.左值引⽤和右值引⽤

  1. 左值引用:只能绑定到已命名的变量,不能绑定到临时对象。例如,std::string &ref = “hello”;是非法的,因为"hello"是一个临时对象。
  2. 右值引用:可以绑定到临时对象,也可以绑定到已命名的变量。例如,std::string &&rref = std::string(“temporary”);是合法的,std::string(“temporary”)是一个临时对象。

• 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

• 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)

• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值

三.左值和右值的参数匹配

  1. C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。

  2. C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。

在C++中,函数参数的匹配规则会根据参数类型(左值引用、右值引用、普通参数等)和传递的实参类型(左值或右值)有所不同。

1. 普通参数

对于普通参数(非引用类型),无论是左值还是右值,都会通过拷贝构造函数或拷贝赋值运算符进行传递。这意味着传递的实参会被复制到函数的形参中。

例如:

void func(int x) {
    // x 是普通参数,实参会被复制到这里
}

int main() {
    int a = 10;
    func(a); // a 是左值,会被复制到 x
    func(20); // 20 是右值,也会被复制到 x
}

2. 左值引用参数

左值引用参数只能绑定到左值。如果尝试将右值绑定到左值引用参数,会导致编译错误。

例如:

void func(int& x) {
    // x 是左值引用参数
}

int main() {
    int a = 10;
    func(a); // 正确,a 是左值
    func(20); // 错误,20 是右值
}

3. 右值引用参数

右值引用参数只能绑定到右值。如果尝试将左值绑定到右值引用参数,会导致编译错误。

例如:

void func(int&& x) {
    // x 是右值引用参数
}

int main() {
    int a = 10;
    func(a); // 错误,a 是左值
    func(20); // 正确,20 是右值
}

4. 万能引用

C++11引入了万能引用的概念,它允许一个引用既可以绑定到左值,也可以绑定到右值。通用引用通过模板参数和&&实现。

例如:

template <typename T>
void func(T&& x) {
    // x 是通用引用
}

int main() {
    int a = 10;
    func(a); // 正确,a 是左值,x 会退化为左值引用
    func(20); // 正确,20 是右值,x 会保持为右值引用
}

在通用引用中,T&&的行为取决于模板参数T的具体类型。如果T是一个具体的类型(如int),T&&就是右值引用;如果T是一个模板参数,T&&就是通用引用。

5. 参数匹配的优先级

当函数重载时,参数匹配的优先级规则如下:

  1. 完美转发:如果函数模板使用了std::forward,则会根据模板参数的实际类型进行完美转发。
  2. 左值引用优先:如果一个函数接受左值引用参数,而另一个函数接受右值引用参数,那么左值引用函数会优先匹配左值。
  3. 右值引用优先:如果一个函数接受右值引用参数,而另一个函数接受左值引用参数,那么右值引用函数会优先匹配右值。

例如:

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

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

int main() {
    int a = 10;
    func(a); // 输出 "lvalue reference"
    func(20); // 输出 "rvalue reference"
}

下面的代码能够总的说明参数匹配这一规则:

#include<iostream>

using namespace std;

void f(int& x)
{
 std::cout << "左值引⽤重载 f(" << x << ")\n";
}

void f(const int& x)
{
 std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}

void f(int&& x)
{
 std::cout << "右值引⽤重载 f(" << x << ")\n";
}

int main()
{
 int i = 1;
 const int ci = 2;
 f(i); // 调⽤ f(int&) 
 f(ci); // 调⽤ f(const int&) 
 f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&) 
 f(std::move(i)); // 调⽤ f(int&&) 
 // 右值引⽤变量在⽤于表达式时是左值 
 int&& x = 1;
 f(x); // 调⽤ f(int& x) 
 f(std::move(x)); // 调⽤ f(int&& x) 
 return 0;
}

四.右值引⽤和移动语义的使⽤

4.1回顾左值引用出现的意义

左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。

在下面代码中,若是没有优化的情况,vv在这里返回时要重新申请空间,进行构造,所花费的代价实际是特别的大的。在之前我们要解决这个问题,就会多一个参数,传过去一个vv。

C++98这里的传值返回拷贝代价就太大了,C++11之后效率就很高,不用担心效率
vector<vector<int>> generate(int numRows) {
	vector<vector<int>> vv(numRows);
	for (int i = 0; i < numRows; ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < numRows; ++i)
	{
		for (int j = 1; j < i; ++j)
		{
		vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
	return vv;
}

 C++11之前,得通过输出型参数改善效率
void generate(int numRows, vector<vector<int>>& vv) {
	//vector<vector<int>> vv(numRows);
	vv.resize(numRows);
	for (int i = 0; i < numRows; ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < numRows; ++i)
	{
		for (int j = 1; j < i; ++j)
		{
			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
}

这个问题,在c++11之后是不需要担心的,编译器会自行优化。

4.2移动构造和移动赋值

  • 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。

  • 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。

  • 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。

4.2.1移动构造和拷贝构造

参数类型:

  • 拷贝构造函数:参数是一个左值引用(const),绑定到左值。
  • 移动构造函数:参数是一个右值引用(&&),绑定到右值。
一、拷贝构造函数
1. 定义

拷贝构造函数是一种特殊的构造函数,它用于通过已存在的对象初始化一个新对象。拷贝构造函数的参数是一个同类型的对象的引用,通常是常量引用(const),以避免修改源对象。

2. 作用
  • 深拷贝:拷贝构造函数通常用于实现深拷贝,即创建一个新对象,并将源对象的资源复制到新对象中。这样,新对象和源对象各自拥有独立的资源,互不影响。
  • 对象初始化:用于通过已存在的对象初始化一个新对象,确保新对象的初始状态与源对象一致。
二、移动构造函数
1. 定义

移动构造函数是C++11引入的一种新的构造函数,它用于通过一个右值(通常是临时对象)初始化一个新对象。移动构造函数的参数是一个同类型的对象的右值引用(&&)。通过将临时对象交换,来达到构造的效果

2. 作用
  • 资源接管:移动构造函数用于直接接管源对象的资源,而不是进行深拷贝。这样可以避免不必要的资源分配和拷贝操作,提高性能。
  • 临时对象处理:移动构造函数特别适用于处理临时对象,因为临时对象的资源在表达式结束后会被销毁,通过移动构造函数可以将这些资源直接转移到新对象中。

4.3解决传值返回问题

  1. 右值对象构造,只有拷⻉构造,没有移动构造的场景
    左边为不优化的情况,右边是编译器优化后的情况
    在这里插入图片描述
  2. 右值对象构造,有拷⻉构造,也有移动构造的场景
    左边为不优化的情况,右边是编译器优化后的情况
    在这里插入图片描述
    在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。
    在这里插入图片描述
  3. 右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
    在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
    在这里插入图片描述
  4. 右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
    vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造
    要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
    在这里插入图片描述

4.4引⽤延⻓⽣命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

int main()
{
	 std::string s1 = "Test";
 	// std::string&& r1 = s1; // 错误:不能绑定到左值 
 	const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期 
 	// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改 
 	std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期 
 	r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改 
 	std::cout << r3 << '\n';
 	return 0;
}

4.5在传参中的提效

当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象

当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上

1. 优化值传递

在C++11之前,值传递会创建参数的副本,这在处理大型对象时会导致不必要的拷贝操作。通过右值引用和移动语义,我们可以优化值传递,避免不必要的拷贝操作。
例如:

void process(std::vector<int> vec) {
    // 使用 vec
}

在C++11中,编译器会自动优化这个函数,避免不必要的拷贝操作。如果vec是一个临时对象,编译器会直接调用移动构造函数,而不是拷贝构造函数。

2. 优化引用传递

在C++11之前,引用传递不能处理临时对象,且不能修改源对象。通过右值引用和移动语义,我们可以优化引用传递,处理临时对象,并允许修改源对象。
例如:

void process(std::vector<int>&& vec) {
    // 使用 vec
}

在这个例子中,vec是一个右值引用,可以绑定到临时对象,允许我们直接操作临时对象的资源。
3. 通用引用
C++11引入了通用引用的概念,允许一个引用既可以绑定到左值,也可以绑定到右值。通用引用通过模板参数和&&实现。
例如:

template <typename T>
void process(T&& vec) {
    // 使用 vec
}

在这个例子中,T&&是一个通用引用,可以绑定到左值或右值。通过std::forward,我们可以实现完美转发,根据实际参数类型调用相应的函数。

五.类型分类在这里插入图片描述

  1. C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。

  2. 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。

  3. 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如move(x)、static_cast<X&&>(x)

  4. 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

在这里插入图片描述

六.引用折叠

  1. C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引⽤的引⽤。

  2. 通过模板或typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。

引用折叠的规则

引用折叠的规则确保了在模板函数中,引用类型能够正确地解析。引用折叠的规则如下:

1. 基本规则

  • T& & 折叠为 T&
  • T& && 折叠为 T&
  • T&& & 折叠为 T&
  • T&& && 折叠为 T&&

2. 通用引用的折叠

在模板函数中,通用引用的折叠规则如下:

  • 如果模板参数 T 是一个具体的类型(如 int),则 T&& 是右值引用。
  • 如果模板参数 T 是一个引用类型(如 int&int&&),则 T&& 会根据引用折叠规则进行折叠。

例如:

template <typename T>
void func(T&& x) {
    // x 是通用引用
}

通过以下代码可以很好的理解引用折叠:

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤ 

template<class T>

void f1(T& x)
{}

// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤ 

template<class T>

void f2(T&& x)
{}

int main()
{
 typedef int& lref;
 typedef int&& rref;
 int n = 0;
 lref& r1 = n; // r1 的类型是 int& 
 lref&& r2 = n; // r2 的类型是 int& 
 rref& r3 = n; // r3 的类型是 int& 
 rref&& r4 = 1; // r4 的类型是 int&& 
 // 没有折叠->实例化为void f1(int& x) 
 f1<int>(n);
 f1<int>(0); // 报错 
 // 折叠->实例化为void f1(int& x) 
 f1<int&>(n);
 f1<int&>(0); // 报错 
 // 折叠->实例化为void f1(int& x) 
 f1<int&&>(n);
 f1<int&&>(0); // 报错 
 // 折叠->实例化为void f1(const int& x) 
 f1<const int&>(n);
 f1<const int&>(0);
 // 折叠->实例化为void f1(const int& x) 
 f1<const int&&>(n);
 f1<const int&&>(0);
 // 没有折叠->实例化为void f2(int&& x) 
 f2<int>(n); // 报错 
 f2<int>(0);
 // 折叠->实例化为void f2(int& x) 
 f2<int&>(n);
 f2<int&>(0); // 报错 
 // 折叠->实例化为void f2(int&& x) 
 f2<int&&>(n); // 报错 
 f2<int&&>(0);
 return 0;
}
template<class T>

void Function(T&& t)
{
 int a = 0;
 T x = a;
 //x++;

 cout << &a << endl;
 cout << &x << endl << endl;
}

int main()
{
 // 10是右值,推导出T为int,模板实例化为void Function(int&& t) 
 Function(10); // 右值 
 int a;
 // a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) 
 Function(a); // 左值 
 // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) 
 Function(std::move(a)); // 右值 
 const int b = 8;
 // a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& 
t)

 // 所以Function内部会编译报错,x不能++ 
 Function(b); // const 左值 
 // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& 
t)

 // 所以Function内部会编译报错,x不能++ 
 Function(std::move(b)); // const 右值 
 return 0;
}

七.完美转发

完美转发是指在函数调用过程中,将参数的类型和值类别(左值或右值)完整地传递给另一个函数。这意味着,如果传递给函数的是一个左值,那么被调用的函数也会接收到一个左值;如果传递的是一个右值,那么被调用的函数也会接收到一个右值。在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到