右值引用、移动语义、完美转发

右值引用、移动语义、完美转发

1 左值与右值

1.1 左值

特点 : 可以在等号左边,能够取地址,具名

例如:变量名 返回左值引用的函数调用 前置自增/减
赋值运算或复合赋值运算 解引用(*)等。

#include<iostream>
using namespace std;
//返回左值引用
int& fun1() {
	int a = 1;
	cout << "fun1" << endl;
	return a;
}

int main() {
	//左值可在等号左边,也可在等号右边
	int a;	//变量名

	a = fun1();	//返回左值引用的函数调用
	fun1() = 2;
	
	++a = 2;//前置自增
	--a = 2;//前置自减

	int b = 2;
	(a = b) = 2;//赋值运算
	(a+=2)=1;//复合赋值运算

	int* p = &a;
	*p = 4;		//解引用

	return 0;
}

1.2 右值

特点:只能在等号右边,不能取地址,不具名

右值又分为:纯右值、将亡值

纯右值:字面值 ,返回非引用类型的函数调用,后置自增/减,算术表达式,逻辑表达式,比较表达式等。


#include<iostream>

using namespace std;

int& fun1() {
	int a = 1;
	cout << "fun1" << endl;
	return a;
}
int fun2() {
	cout << "fun2" << endl;
}
int main() {
	//右值只能在等号右边,在左边编译器报错
	int a = 100;//这里100为右值
	100 = a;	//不能做左值,报错

	a = fun2();//返回非引用类型的函数调用
	fun2() = 2;//不能做左值,报错

	a++;
	a++ = 2;//后置自增/减
	a-- = 2;
		
	(a + 2) = 1;	//算术表达式

	(a || 1) = 2;	//逻辑表达式

	(a < 2) = 2;	//比较表达式
	return 0;
}

将亡值:

  1. ​ C++11 新引进的与右值引用(移动语义)相关的值类型,
  2. ​ 将亡值用来触发移动构造函数或移动赋值构造,并进行资源转移,之后将调用析构函数
#include<iostream>

using namespace std;

class T
{
public:
	T() {
		cout << "T():" << this << endl;
	}
	~T() {
		cout << "~T():" << this << endl;
	}
	T(const T&) {
		cout << "(const T&)拷贝构造:" << this << endl;
	}
	void operator=(const T&) {
		cout << "(operator=(const T&))拷贝赋值构造:" << this << endl;
	}
	T(T&&) {
		cout << "T(T&&) 移动构造:" << this << endl;
	}
	void  operator=(T&&) {
		cout << "operator=(T&&)移动赋值构造:" << this << endl;
	}
private:
	int i;
};

T CreateT() {
	T temp;
	return temp;
}

int main() {
	if (false) {//拷贝构造
		T t1;
		T t2 = t1;

		T t3(t1);

		T t4(t1);
	}
	if (false) {//拷贝赋值构造
		T t1;
		T t2;
		t1 = t2;
	}
	if (false) {//移动构造
		//需要禁用返回优化:-fno-elide-constructors
		//1.看类有没有移动构造
		//2.然后看类有没有拷贝函数
		//3.报错

		//调用移动构造

		T t= CreateT(); //,如果去掉移动构造函数的话,调用的是拷贝构造函数
        
		T t2(std::move(t)); //move() 将左值转为右值

		T t3(CreateT());//这里CreateT()返回的是右值
	}
	if (true) {//移动赋值函数
		T t;
		t = T();
	}
	return 0;
}

注意:const左引用能指向右值,局限不能修改该值 T(const &T)

(1)执行CreateT();,发现调用一次普通构造和移动构造,很明显,在函数里调用普通构造,而返回是调用移动构造,此时函数CreateT里的temp就是将亡值,即调用该析构函数

在这里插入图片描述

(2)执行T t =CreateT(); 发现比上面多调用了一次移动构造,
为什么不是多调用一次拷贝构造函数呢?
因为CreateT()是一个右值,系统则优先调用移动构造函数

在这里插入图片描述

(3)move()将t 这个左值转化为右值,则优先调用移动构造函数

在这里插入图片描述

(4)与(2)相同

在这里插入图片描述

2 左值引用与右值引用

2.1 引用

  1. 做别名
  2. 声明时必须要初始化
  3. 通过引用修改修改变量值

2.2左值引用

2.21 定义及使用

左值引用是对左值的引用(左值引用是左值)

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

作用:左值引用用来避免对象拷贝,函数传参,函数返回值
传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。

使用场景:

// 1.左值引用做参数
void func1(string s)
{...}

void func2(const string& s)
{...}


int main()
{
	string s1("Hello World!");
	func1(s1);  // 由于是传值传参且做的是深拷贝,代价较大
	func2(s1);  // 左值引用做参数减少了拷贝,提高了效率
    // 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
    string s2("hello");
    // string operator+=(char ch)  传值返回存在拷贝且是深拷贝
    // string& operator+=(char ch)  左值引用做返回值没有拷贝,提高了效率
    s2 += '!';

	return 0;
}
2.22 局限

左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。

当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。

string operator+(const string& s, char ch)
{
	string ret(s);
	ret.push_back(ch);
	return ret;
}

// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题

于是,对于第二种情形,左值引用也无能为力,只能传值返回。

2.3 右值引用

2.31 定义

右值引用是对右值的引用(右值引用是左值)

右值引用的表示是在具体的变量类型名称后加两个 &,比如:int&& rr = 4;。右值引用的表示是在具体的变量类型名称后加两个 &,比如:int&& rr = 4;

注意:
右值引用引用右值,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。

比如:

double&& rr2 = x + y;
&rr2;
rr2 = 9.4;
//右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。
const double&& rr4 = x + y;
&rr4;
//可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错。

了解右值引用的使用还得了解移动语义以及完美转发

2.4 实现移动语义

2.41 移动构造函数

在上面1.2右值一节中,其中的拷贝构造和移动构造

  1. 拷贝构造函数的参数是 const左值引用,接收左值或右值;
  2. 移动构造函数的参数是右值引用,只接收右值或被 move 的左值。

注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数,因为其可以少做一次深拷贝

2.42 STL应用

在STL使用加个std::move会调用到移动语义函数,避免了深拷贝。

除非设计不允许移动,STL类大都支持移动语义函数,即可移动的。 另外,编译器会默认在用户自定义的classstruct中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造等函数。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

#include<iostream>
#include<list>
#include<string.h>
using namespace std;

class A{
public:
    A() {
		
        p= new int(10);
        cout << "A():"<<p << endl;
	}
	~A() {
		cout << "~A():" ;
        if(p != nullptr){
            delete[] p;
            p = nullptr;
            cout<<"~A1()"<<endl;
        }
        else{
            cout<<"~A2()"<<endl;
        }
	}
	A(const A& a) {
		cout << "(const A&)拷贝构造:" <<endl;
        p = new int(10);
        memcpy(p,a.p,10*sizeof(int));
	}
	void operator=(const A& a) {
		cout << "(operator=(const A&))拷贝赋值构造:" <<endl;
        p = new int(10);
        memcpy(p,a.p,10*sizeof(int));
	}
	A(A&& a) {
		cout << "A(A&&) 移动构造:" << endl;
        p = a.p;
        a.p = nullptr;

	}
	void  operator=(A&& a) {
		cout << "operator=(T&&)移动赋值构造:" << endl;
        p = a.p;
        a.p = nullptr;
	}
    int *p;
};
void func(int && a ){
    cout<<"rvalue = "<< a << endl;
}


int main(){
    list<A> alist;
    alist.push_back(A());//A()为匿名函数
    auto &ele = alist.front();//获取头部元素
    cout<<"ele.p:"<<ele.p<<endl;


    return 0;
}

结果调用的是移动构造,而不是拷贝构造

在这里插入图片描述

2.43 unique_ptr智能指针

智能指针也实现了移动语义

2.5 完美转发

2.51 解释

  1. 函数模板可以将自己的参数完美地转发给内部调用的其他函数
  2. 完美是指不仅能准确地转发参数的值,还能保证被转发的参数的左右值属性不变

这里的remoke函数实现了完美转发。

在这里插入图片描述

2.52 万能引用

借用万能引用,通过引用的方式接收左右属性的值

引用折叠规则:

  1. 参数为左值或左值引用,T&&将转化为int &

  2. 参数为右值或右值引用,T&&将转化为int &&

系统推导的类型为万能引用,如 T&& t,auto &&t
template<typename T> void remoke(T&& t);
remoke(n);//相当于func(n)
此时T识别为int&,int& &&t ==> int &t;

remoke(100);//相当于func(100)
此时T识别为int&,int& &&t ==> int &t;

remoke(static_cast<int &> n);//强制转换为左值引用
此时T识别为int&,int& &&t ==> int &t;

remoke(static_cast<int &&> n);//强制转换为右值引用,并使其变成右值
此时T识别为int&&,int&& &&t ==> int &&t;

2.23 用forward<>()来实现透传

上面我们实现了传参,如果没有forward函数的话,无论我们传的是左值还是右值,调用的都是func(int & n),因为T&& t推导出来的t,只能是左值引用或者右值引用

左值引用与右值引用都是左值

#include<iostream>
using namespace std;
void func(int& n ){
    cout<<"rvalue = "<< n << endl;
}
void func(int&& n){
    cout <<"lvalue = "<< n <<endl;
}

template<typename T>
void remoke(T&& t){
    func(t);
}
int main(){
    int i=10;
    remoke(100);  //传一个右值
    remoke(i);	//传一个左值
    return 0;
}

在这里插入图片描述

那我们如何来保证左右值属性不变呢?使用forward函数
修改remoke函数

template<typename T>
void remoke(T&& t){
    func(forward<T>(t));
}

结果说明左右值属性传到了

在这里插入图片描述

这里我们再传左值引用和右值引用

int main(){
    int i=10;
    remoke(100);
    remoke(i);

    int a =11;
    int &b =a;
    int &&c = 111;
	//static_cast<>()为类型强转
    remoke(static_cast<int&>(b));//传入左值引用
    remoke(static_cast<int&&>(c));//传入右值引用
    
    return 0;
}

在这里插入图片描述

结果说明,传入左值引用或右值引用也成功了

2.24 问题

这里为什么要使用强转呢?如果你去掉强转,会发现传入参数c调用的是fun(int &)函数, 因为右值引用c是左值,所以T识别为int&

万能引用只是提供接收的左值和右值的能力,并将参数转化为对应的左右值引用。但是左值引用和右值引用作为参数时,c++后续使用都会被处理为左值, 因此调用左值模板函数。

因为 c 是一个左值,而static_cast<int&&>©是一个右值。

在这里插入图片描述

所以这里我严重怀疑

这与forward使用情形是差不多的,于是我想能不能让satic_cast来代替forward,结果居然是一样的

在这里插入图片描述

然后来看看forward的源码

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
    remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}
 
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}

我们看到了他是个重载函数,通过万能引用和引用折叠,返还出我们的值,我们传入的值无非就是,带引用和不带引用两种,第二个它加了个断言用于不加引用的版本以防出错。我们看到了std::forward很简单,当传入参数为不是引用或为右值引用我就强转为右值引用,其他强转为左值引用。

但是你在传参时,还是避免不了要指定int&&引用类型,因为它不会因为他是右值引用类型而去强转成右值

在这里插入图片描述

在这里插入图片描述

2.25 static_cast<\T>()与forward<\T>()

在上面,我实在不明白static_cast俩种强转,于是我在函数里增加了自增

#include<iostream>
using namespace std;


void func(int& n ){
    cout<<"lvalue = "<< n++ << endl;
}
void func(int&& n){
    cout <<"rvalue = "<< n++ <<endl;
}

template<typename T>
void remoke(T&& t){
    func(static_cast<T>(t));
}

int main(){

    int a =11;
    int &b =a;
    int &&c = 111;
    remoke(static_cast<int&>(b));//传入左值引用
    remoke(static_cast<int&&>(b));//传入右值引用
    remoke(b);
    remoke(b);
    cout<<"a="<< a <<endl;
    return 0;
}

运行结果截图

在这里插入图片描述

这里我发现忘记把remoke函数里的static_case改回forward了,改回来再运行一下发现结果不同了:

在这里插入图片描述

这里static_cast使他失去了引用的功能?(修改a的值)

于是我多次执行下面这条语句,发现a修改了

func(static_cast<int&&>(b));
func(static_cast<int&&>(b));
func(static_cast<int&&>(b));
cout<<"a="<<a<<endl;
结果
rvalue = 11
rvalue = 12
rvalue = 13
a=14

那为什么上面不会修改?最后还是搞不懂,希望有人能帮我解惑

致谢

参考了很多(还有一些忘了在哪了),这里表示感谢

大厂面试讲解 C++11 面试题总结(左/右值引用、新特性、智能指针、类型推导、override,final关键字)_哔哩哔哩_bilibili

(115条消息) (转)一文读懂C++右值引用和std::move_右值引用的原理和本质_BBBourne的博客-CSDN博客

(115条消息) 详解 C++ 左值、右值、左值引用以及右值引用_c++ 引用 左值引用 右值引用_Hoshino373的博客-CSDN博客

End,如有更好的理解和思路请多多交流。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值