C++右值引用

2023-12-16 04:57:20

左值和右值的概念

通俗一点来说,左值就是可以出现在等号(赋值语句"=")左边的值,而右值就是只能出现在等号的右边,左值既可以出现在等号右边也可以出现在等号的左边。一个便于记忆的方式是,左值是可寻址的变量,有持久性。右值一般是不可寻址的常量,是短暂性的。左值一般为实体化的变量或者对象,而右值一般为常量、临时变量或者对象等。

对于左值和右值的概念这里不再过多的展开叙述,只是粗略地介绍,以便对后面内容的理解,如果想要细究左值和右值的概念,可以参考这篇文章:理解 C/C++ 中的左值和右值

右值引用

在C++11之前,只有一种引用方式,C++11又提出了另一种引用方式:右值引用。那么之前的那种方式自然就叫做左值引用了。

右值引用的写法很简单,左值引用是一个&,右值引用是两个&。比如之前我们写一个引用时是这样的(左值引用):

int num = 100;
int& l_ref = num;

C++11之后我们也可以直接用右值引用,比如这样:

int&& r_ref = 100;

需要注意的是,虽然右值引用是引用的右值,但右值引用的这个引用本身却是一个左值。以上面的代码为例,100是右值,r_ref是一个右值引用,但r_ref的本身的属性却是一个左值。所以有时我们想要递归地使用一个右值引用参数的函数时,会莫名其妙地调用到左值引用或者其它版本。对于这个问题可以用完美转发(forward)或者move函数解决。

不管是左值引用还是右值引用,它们都是引用,所以它们的底层都是封装了指针。也就是说右值引用的本质也是一个指向某块区域的指针。而右值引用的底层实现上,是会为对应的右值在栈帧上开辟了空间的,其底层的指针就指向这块空间并进行操作的。

move函数

一般来说,非const左值引用只能绑定左值,右值引用只能引用右值。如果想要让右值引用绑定到左值的话,可以使用move函数来间接做到。

move函数的功能是,接受一个参数,返回它的右值引用。需要注意的是,move函数并不会改变传入参数的属性,只是返回具有相同内容的右值引用。以下面的这段代码为例

int num = 100;
int&& ref = move(num);

num是一个左值,将其作为move函数的参数时,并不会改变num的左值属性,而是move函数会返回一个具有相同内容的右值引用,随之赋值给ref。

使用右值引用,可以有效的减少拷贝。传统的左值引用对应着具名对象,而右值引用对应着临时对象或表达式的结果。通过使用右值引用,可以直接将资源(如内存)的所有权转移给新的对象,而不需要执行昂贵的拷贝操作。

移动语义

减少拷贝只是右值引用的一个优点之一,右值引用的另一个核心内容就是移动语义。这里的移动指的是允许将资源从一个对象“移动”到另一个对象,而不是执行传统的深层拷贝。具体点说,移动语义指的就是移动构造和移动赋值。

这里以移动构造为例,移动构造一般是指移动拷贝构造。移动构造的特殊之处在于,它的参数是一个右值引用。例如有如下的person类

class person
{
public:
    // 普通的拷贝构造
	person(const person& other) {
		……
	}
    // 移动拷贝构造
	person(person&& other) {
		……
	}
};

那么如果我们在调用构造函数时,如果传入的是一个右值(比如一个临时对象),那么就会自动匹配调用对应的移动拷贝。一般来说,移动拷贝可以通过swap之类的操作,将右值引用中的资源给转移出来。例如:

万能引用

一般情况下,类型后跟两个&&表示的是右值引用,但在涉及到类型推导的情况下,表示的就是万能引用。万能引用的意思是,根据具体传入的是左值还是右值来推导对应的类型属性。

有很多地方会误解为函数模板下的为万能引用,但其实本质上核心的关键点在于需要有类型推导。比如下方这几个示例:(示例参考:浅谈C++11万能引用和右值引用 - 知乎 (zhihu.com)

// 1.
void f1(Foo&& p);

// 2.
Foo&& var2 = Foo();

// 3.
auto&& var3 = var1;

// 4.
template<typename T>
void f4(std::vector<T>&& p);

// 5.
template<typename T>
void f5(T&& p);

其中只有3和5是万能引用,1和2并不涉及任何类型推导,而4看似像是万能引用,但实际上4是指明了函数的参数类型为vector的,所以并不是万能引用。

完美转发

前面我们提到,右值引用虽然绑定的是一个右值,但是右值引用本身却是一个左值,如果我们想要让函数传参时的参数仍然保持原理的性质,就可以使用完美转发,forward函数来实现。forward是一个函数模板,而且forward的具体实现有两种,一种是参数为左值引用的,一种是参数为右值引用的。其返回值解释如下,如果参数类型为右值,则返回对应的右值引用;参数类型为左值,则返回对应的左值引用。所以通过上述内容,forward边实现了传入哪种引用最终就返回的哪种属性。

而且,从技术层面上讲,forward确实可以替代move函数,但所谓术业有专攻,如果把forward当作move来用就相对比较麻烦,使用成本较高。所以虽然forward可以完成move的工作,但一般情况下我们都不会选择这样用。

一个完美转发的用法示例如下:

void show(int cnt, const int& num)
{
	if (cnt >= 0)
	{
		cout << num << " | " << "左值引用版本" << endl;
		show(--cnt, num + 1);
	}
}

void show(int cnt, int&& num)
{
	if(cnt >= 0)
	{
		cout << num++ << " | " << "右值引用版本" << endl;
		show(--cnt, forward<int>(num));
	}
}

int main()
{
	show(10, 101);
	return 0;
}

注意事项

  1. 右值引用也是构成函数重载的一个条件。
  2. C++11之前,如果不写,编译器会为每个类生成6个默认成员函数。C++11之后,又新增了两个默认成员函数,分别是默认移动拷贝构造函数和默认移动赋值运算符重载函数。这两个默认生成的函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  3. 在没有手动定义移动拷贝构造函数或者移动赋值重载函数的前提下,且没有实现析构函数、拷贝构造、赋值重载中的任意一个。那么编译器才会生成对应的默认函数。

文章来源:https://blog.csdn.net/m0_73759312/article/details/132030317
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。