C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现

目录

一.右值引用

(一).何为右值

(二).右值引用

(三).右值和左值的互相传递

①左值->右值引用

②右值->左值引用

(四).右值引用的自身属性

二.移动构造和移动赋值

 (一).移动构造

 (二).移动赋值

三.转发

(一).万能引用

(二).完美转发

四.move和forward底层实现方式

(一).move底层实现

(二).forward底层实现


一.右值引用

(一).何为右值

不能取地址的就是右值。例如:字面常量、临时变量。

//1就是右值
int i = 1;
//max返回值是临时变量,也就是右值
int n = max(1, 2);

(二).右值引用

左值引用是对左值的引用,顾名思义,右值引用就是对右值的引用。

右值引用符号为&&,使用方式与左值引用相同,但符号和引用对象属性不同。

左值引用 右值引用
符号 & &&
引用对象 左值(可取地址的变量) 右值(不可取地址)
使用方式

int j = 1;

int& a = j;

int&& a = 1;

(三).右值和左值的互相传递

左值和右值无法直接传递。

int main() 
{
	int a = 1;
	int& b = a;//正确,左值引用

	int&& c = b;//错误。右值引用不能传左值

	int& d = 1;//错误。左值引用不能传右值
	return 0;
}

①左值->右值引用

该函数参数为左值,返回值为右值。

作用是将左值参数强制转化成右值引用。 

int a = 1;
int&& c = move(a);//将a强转成右值

②右值->左值引用

左值引用加上const修饰符即可。

const int& d = 1;

(四).右值引用的自身属性

右值引用本身是左值属性。

因此,如果给左值引用传递右值引用是可以的。

int&& a = 1;//a为右值引用,但自身属性是左值
int& b = a;//正确,给左值引用传递左值

这该怎么理解呢?

《C++ Primer》对此给出了相关解释,大意如下:

左值是“持久”的,右值是“短暂”的。即左值只要不出作用域就可以一直存在,但右值只能在使用时的瞬间“存活”(参考函数返回值)。

因此,当进行右值引用后,引用本身可以一直在作用域中存在,那么它就是左值。

当然,可以使用另一种方式证明:取地址。

左值可以取地址,右值不可以取地址。

int main() 
{
	int a = 1;//a可以取地址,是左值
	int&& b = 3;
	cout << "a地址: " << &a << endl;
	cout << "b地址: " << &b << endl;
	return 0;
}

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第1张图片

 不妨总结一下:

左值 右值 左值引用 右值引用
举例 int a = 1; string str = "abc"; int& b = a; int&& c = 1;
属性 左值 右值 左值 左值
取地址 不能
转化

接收右值:

直接传

接收左值:

接收右值:

+const

接收左值:

move函数 

二.移动构造和移动赋值

C++11引入右值引用后,很大的作用便是移动构造与赋值。

比如官方STL库中就提供了相关函数:

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第2张图片

 (一).移动构造

移动构造的目的在于减少因为参数是左值时引发的重复拷贝的问题。 

以string为例进行说明:

截取如下代码:

class String {

	...

	explicit String(const char* a = "")//默认构造
	{
		_size = strlen(a);
		_capacity = _size;
		_a = new char[_capacity + 1];
		strcpy(_a, a);
		cout << "构造函数\n";
	}

	String(const String& st)//拷贝构造
		:_a(nullptr)
	{
		String tmp(st.c_str());//调用构造函数
		swap(tmp);
		cout << "拷贝构造\n";
	}

    ...

	
};
String To_string(int value)//将int转为string
{
    ...
    String str;
    ...
    return str;
}
int main()
{
	String str = To_string(20);
	return 0;
}

当我们执行这个程序时,会调用2个默认构造和1个拷贝构造:

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第3张图片分别是to_string内部生成str时调用默认构造、返回临时变量时调用string拷贝构造,但是string拷贝构造内部又会先调用默认构造。

其实这还是优化后,如果没有编译器优化,main函数中str也会再调一次string拷贝构造。

 而这一切的“罪魁祸首”是什么呢?——to_string的返回值。

是的,因为to_string内部会生成一个string对象,而该对象是局部变量,出了函数作用域就销毁,因此只能调用拷贝构造to_string内部的对象。

这还只是string类型拷贝构造,如果是更加复杂的类型,拷贝构造往往会造成更多资源的占用。

正因如此,移动构造派上了用场:

String(String&& st)//移动构造函数,但是参数为右值
	:_a(nullptr)
{
	swap(st);
	cout << "string移动构造\n";
}

移动构造的参数为右值,所以当to_string返回str时,会被移动构造接收。

虽然str本身为左值属性,但是因为此时str是“将亡值”,即出了函数作用域就会被销毁,编译器会将这种“即将死亡”的值识别为右值。

在移动构造内部,会将右值的数据与自身数据进行交换。因为右值作为“暂时存在的数据”,把数据交给目标对象,目标对象把“舍弃”的数据交给右值,正好可以“延续”目标数据且消除原本数据。

这时,接收to_string返回值时只需要一个一个移动构造即可: 

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第4张图片

 (二).移动赋值

移动赋值的目的与移动构造类似,在于减少因为赋值造成重复拷贝的问题

以string为例,其中赋值重载通过调用了拷贝构造函数实现。

class String {

	...

	String& operator=(const String& st)//赋值重载1
	{
		String tmp(st);//调用拷贝构造
		swap(tmp);
		cout << "string赋值\n";
		return *this;
	}
	String& operator=(const char* str)//赋值重载2
	{
		String tmp(str);
		swap(tmp);
		cout << "char*赋值\n";
		return *this;
	}

    ...

	
};
int main()
{
	String str;
	cout << "--------------------------------\n";
	str = To_string(1);
	return 0;
}

 当执行这个程序时,会有多个构造、拷贝构造被调用:

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第5张图片

 而这其中,属于因为赋值重载而调用的就有三个。

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第6张图片 因为赋值重载的参数是左值引用,不能像右值引用那样交换数据,只能调用拷贝构造获取数据。

由此,移动赋值应运而生:

与移动构造相同,移动赋值也是直接与右值交换数据。 

String& operator=(String&& st)
{
	swap(st);
	cout << "string移动赋值\n";
	return *this;
}

C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现_第7张图片

 此时,只需要将to_string的返回值作为右值传给移动赋值即可。

三.转发

(一).万能引用

首先,万能引用只存在与模板编程中

万能引用就是引用形参既可接收左值也可接收右值,其符号与右值引用相同,但必须是模板。

即当模板的参数是右值引用的形式,如果实参是左值就是左值引用,右值就是右值引用。

 例如下列代码:

void Print(int& a)
{
	cout << "左值" << endl;
}

void Print(int&& a)
{
	cout << "右值" << endl;
}

template
void func(T&& t)//万能引用
{
	Print(t);
}

int main()
{
	int a = 0;
	func(a);//传左值
	func(1);//传右值
	return 0;
}

(二).完美转发

上述代码有一个问题,尽管func(1)传入的是右值,但是因为右值引用本身是左值,当调用Print函数时,会调用左值版本,这不符合我们的预期,因为明明传入的是右值:

 这时,就需要使用完美转发forward,它会保持传入实参的属性不变:

void func(T&& t)
{
	Print(std::forward(t));
}

四.move和forward底层实现方式

(一).move底层实现

首先看一下move函数底层代码:

template 
typename remove_reference::type&& move(T&& t)
{
    return static_case::type&&>(t);
}

其中参数T&&是万能引用,可接收左值或右值。

返回值很特殊,typename remove_reference::type的含义就是去掉T的引用类型

remove_reference本身是模板类,它的作用就是返回一个类型,所以这个类里面只有成员类型

通过remove_reference源码可以看到,不管传入的是左值引用还是右值引用,它都只会返回这个值去掉引用后的类型。

我们以int为例,不管传入int&还是int&&,经过remove_reference后,返回的都是int。

template 
struct remove_reference{
    typedef T type;  //成员类型
};

template 
struct remove_reference //左值引用
{
    typedef T type;//返回T本身的类型
}

template 
struct remove_reference //右值引用
{
   typedef T type;//返回T本身的类型
}

static_case作用是强制类型转换,可以将左值强转成右值,move中是强转成右值引用。

因此,move底层代码可以翻译成如下形式:

template 
int&& move(T&& t)
{
    return (int&&)(t);
}

于是,我们清楚的发现:move函数就是通过remove_reference获取引用对象本身的类型,强转成右值引用的方式实现的

(二).forward底层实现

这是forward底层代码:

template 
T&& forward(typename std::remove_reference::type& param)//左值引用
{
    return static_cast(param);//万能引用
}

template 
T&& forward(typename std::remove_reference::type&& param)//右值引用
{
    return static_cast(param);//万能引用
}

 有了move的基础,forward就不难理解了。

它通过remove_reference来区分传入的参数是左值引用还是右值引用,然后调用具体的重载forward函数。

再通过万能引用的形式,根据param的具体类型返回左值引用还是右值引用。

源码可以翻译成如下形式(int为例):

template 
T&& forward(int& param)//左值引用
{
    return (T&&)(param);//万能引用
}

template 
T&& forward(int&& param)//右值引用
{
    return (T&&)(param);//万能引用
}

参考文章:

聊聊C++中的完美转发 - 知乎 (zhihu.com)

C++高阶知识:深入分析移动构造函数及其原理 | 音视跳动科技 (avdancedu.com)

参考书籍:

《C++ Primer》 

程序是我的生命,但我相信爱她甚过爱我的生命。——未名


如有错误,敬请斧正 

你可能感兴趣的:(C++语法,c++,右值引用,左值引用,move/forward底层,移动构造,移动赋值,万能引用,完美转发)