右值引用和移动语义

左值和右值

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们以前写的那些的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。

左值(lvalue)

左值是指可以取地址的表达式,通常具有持久的状态,存储在内存中。左值可以出现在赋值运算符的左侧或右侧。

左值的特点:

  • 可以取地址(使用 & 运算符)
  • 通常有名称(变量名)
  • 生命周期超过当前表达式
  • 可以修改(除非被 const 修饰)

示例:

int a = 1;          // a是左值
double b = 2.2222;  // b是左值
float c = 1.1;      // c是左值
string s("11111");  // s是左值
int* p = &a;        // p是左值

右值(rvalue)

右值是指不能取地址的表达式,通常是临时对象或字面量。右值只能出现在赋值运算符的右侧。

右值分为两类:

  1. 纯右值(prvalue):字面量、临时对象、返回非引用的函数调用
  2. 将亡值(xvalue):即将被销毁的对象,可以通过右值引用绑定

右值的特点:

  • 不能取地址(使用 & 运算符会编译错误)
  • 通常是匿名的临时对象
  • 生命周期仅限于当前表达式
  • 可以被移动(资源可以被"窃取")

示例:

10;                     // 字面量,纯右值
string("hello");        // 临时对象,纯右值
x + y;                  // 表达式结果,纯右值
std::move(s);           // 将亡值
func();                 // 返回非引用的函数调用,纯右值

核心区别

左值和右值的核心区别就是能否取地址。

  • 左值:可以取地址,有持久状态
  • 右值:不能取地址,通常是临时对象

特殊情况

  1. const左值:用 const 修饰的左值不能修改,但可以取地址

    const int x = 10;  // x是const左值
    &x;                // 可以取地址
     x = 20;         // 错误:不能修改
    
  2. 右值引用变量本身是左值:虽然右值引用绑定到右值,但引用变量本身是左值

    int&& rref = 10;  // rref绑定到右值10
    &rref;            // 可以取地址,rref本身是左值
    
  3. 将亡值(xvalue):C++11引入的概念,指即将被销毁但资源可以被移动的对象

    std::string s1 = "hello";
    std::string s2 = std::move(s1);  // std::move(s1)返回将亡值
    

左值引用和右值引用

顾名思义,左值引用就是给左值取别名,右值引用就是给右值取别名。

int a = 10;
int &x = a;//x是a的别名(左值引用)
int &&r = 20;//右值引用
double&& rr = x + y;//右值引用

左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值 为什么,因为左值的属性是可读可写,右值是只读的,左值的权限比右值大,这就属于权限缩小。
右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值) move函数是什么,他是一个模板template typename remove_reference::type&& move (T&&arg); 这个通俗来说就是一个强制类型转换,把左值强制转换为右值,就类似于下面这种

int a = 10;//左值
int &x = a;//左值引用
int &&b = (int&&)a;//强制转换

需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值 为什么会这样设计 原因是保证证可以修改、取地址、复用资源右值引用的作用是接管临时对象资源,我们肯定希望绑定后能操作这个临时对象

左值引用与右值引用比较

1.左值引用可以引用左值但不能引用右值
2. 右值引用不能直接引用左值,但可以引用move之后的左值
3. const 左值引用 const T& 既能绑定左值,也能绑定右值

为什么c++11要设计一个右值?我们首先来看看左值引用的不足
在这里插入图片描述
下面来举一个例子
我们知道如果我们在函数中想返回一个对象就如下如图所示
在这里插入图片描述

这时候就会产生一个临时对象,因为这是传值返回 正常情况下会先拷贝构造给一个临时对象,然后在拷贝构造给
接收的对象,这里就会产生两次拷贝,浅拷贝的类还好,深拷贝的代价就大了,通常情况下编译器会进行优化,变成只有一次拷贝构造变成下面这种情况

在这里插入图片描述
当然,这不是c++委员会规定的,完全就是编译器自己的行为,优不优化,完全取决于编译器,按照上面这种情况,我们可以认为返回的s1是一个将亡值,出了这个函数作用域就销毁了,他本身的资源也就释放了,我们也需要它的资源,拷贝构造的代价太大,有没有什么办法能较好的解决这个问题呢?

C++11认为上述情况的s1是右值,在用s1构造临时对象时,就会采用一个叫做移动构造的东西,即将s1中资源转移到临时对象中(而不是拷贝到临时对象中,不管编译器优不优化,这样消耗的代价就小很多)。而临时对象也是右值,因此在用临时对象构造s2时,也采用移动构造,将临时对象中资源转移到s2中,整个过程,只需要申请一块堆内存空间即可,既省了空间,又大大提高程序运行的效率。

由以上内容可以看出右值引用的关键是为了移动构造和移动赋值所设计的。
下面来说说右值引用的使用场景

移动构造和移动赋值

移动构造和移动复制是什么

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

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

3.对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的自己写的string样例实现了移动构造和移动赋值,我们需要结合场景理解。

下面这个代码就是移动构造

//自己模拟实现的string
void swap(string& s)
{
   std::swap(_str, s._str);
   std::swap(_size, s._size);
   std::swap(_capacity, s._capacity);
}

string(string&& s)
{
   cout << "string 移动构造" << endl;
   swap(s);
}

string func()
{
	return string("123");
}

int main()
{
   string s1 = func();
   return 0;
}

这样看这个代码非常简单,意思就是转移资源我们可以画图看看
在这里插入图片描述
移动赋值也同理

string& operator=(string&& s)
{
   cout << "移动赋值" << endl;
   swap(s);
   return *this;
}

那么这个过程就会变成
在这里插入图片描述

当我们有了移动构造的时候,编译器也会优化,而且会优化的更彻底
在这里插入图片描述
这里怎么验证,可以进行打印s1和s2的地址,发现他们两个是一样的
在这里插入图片描述
总结一下:
右值引用的出现不是直接去使用右值引用来减少拷贝,而是对于像深拷贝的类如vector string map等,在传值返回资源时,可以调用移动构造和移动复制去减少拷贝,来提高效率

由上述结论我们可以看出移动构造和移动赋值是多么的重要,对于深拷贝的类是非常有必要的,实现移动构造和移动赋值是有很大的价值的

引⽤折叠

引用折叠首先要知道一个规矩 c++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引⽤的引⽤。

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

下面写代码来演示一下

在这里插入图片描述
这里记忆起来非常简单只有 && && 两个结合起来才是右值引用,其他都是左值
由于以上特性,我们可以得出一个结论像f2这个函数
在这里插入图片描述
眼看上去是右值引用,实际上这个可以折叠为左值或者右值引用,这个也叫做万能引用,是一个非常有用的东西
我们也需要来仔细理解一下,结合下面的代码看看
在这里插入图片描述

我们可以得出一个结论引用折叠最核心、最常用的场景就是实现万能引用(万能引用 T&&)引用折叠本身是 C++ 类型推导的底层规则,它几乎没有独立使用场景,设计出来就是为了支撑万能引用与完美转发。

完美转发

一句话说清楚 完美转发:在模板函数中,把传入的参数原封不动地转发给内部另一个函数,完整保留参数原本的:
类型(const/ 非const)值类别(左值 / 右值)全程不发生额外拷贝,是什么就传递说明。

我们来看一个代码
在这里插入图片描述
我们先不用完美转发看看结果
在这里插入图片描述

我们发现我们传递的右值都变成了左值引用,这是为什么,再来看看有完美转发的情况
在这里插入图片描述
取消注释
在这里插入图片描述
这下就按照我们的想法走了,我们可以得出结论

右值引用的对象,再作为实参传递时,属性会退化为左值,只能匹配左值引用。使用完美转发,可以保持他的右值
属性

那么这个完美转发有什么用呢,我们可以去看看vector,list这些容器的接口发现有个接口叫emplace_back这个我们后面会讲解

可变参数模板

可变参数模板的用法

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。
像这种就是模板参数包

template<class ...Args>

这种是函数参数包

void func(Args... args)

我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。

有了这些语法支持以后就可以传递任意多个参数了,而不是固定参数,这无疑是一个巨大的进步。
我们可以来计算一下参数包的个数
在这里插入图片描述

那么这个是怎么实现的?
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。只是这个过程交给编译器做了
我们可以来看看它的原理

在这里插入图片描述
一个可变参数模板实例化出多个不同的模板参数,多个不同的函数模板实例化出不同的参数函数

包扩展

什么叫做包扩展,结合上面的函数参数包,可以这么理解,参数包就像一个袋子把所有的参数放在袋子里打包,而包扩展,就是把袋子里的参数依次拿出来,对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。
我们可以来试试
这里怎么拿还是有方法的,来看看下面的代码

void get_args()
{
	;
}
template<class T,class ...Args>
void get_args(T t,Args... arg)
{
	cout << t << endl;
	get_args(arg...);
}
template<class ...Args>
void print(Args... arg)
{
	get_args(arg...);
}

int main()
{
	print(1, "function", 3);
	return 0;
}

我们来画图看看,这个包扩展是怎么一回事
在这里插入图片描述
有了上面的了解之后,我们可以来看看完美转发和可变模板参数是怎么实现emplace_back的

emplace_back接口

在这里插入图片描述
这个接口和push_back的功能一样,参数类型是一个右值万能引用,他比push_back要高效一些,为什么这个我们看一下下面的模拟实现list的emplace_back就知道了



template<class ...Args>
List_node(Args&&... args)
	:_data(forward<Args>(args)...)
	, _next(nullptr)
	, _prev(nullptr)
{
  ;
}


template<class ...Args>
Iterator insert(Iterator pos, Args&&... args)
{
	Node* newnode = new Node(forward<Args>(args)...);
	Node* cur = pos._node;
	Node* prev = cur->_prev;

	newnode->_next = cur;
	cur->_prev = newnode;

	newnode->_prev = prev;
	prev->_next = newnode;

	++_size;
	return Iterator(newnode);
}
template <class... Args>
void emplace_back(Args&&... args)
{
	insert(end(), std::forward<Args>(args)...);
}

我们来分析一下这段代码
在这里插入图片描述
在这里插入图片描述
这样emplace_back就比push_back稍微快一点,所以更加推荐使用emplace_back

lambda表达式

lambda是什么,说这个之前我们先提一下一个东西叫仿函数,仿函数是一个类重载了operator()的类,我们可以像函数一样调用这个类,就像下面的代码

class add
{
public:
	int operator()(int x, int y)
	{
		return x + y;
	}
};
int main()
{
	int ret = add()(10,20);
	cout << ret << endl;
	return 0;
}

现在来说说lambda表达式,这个的底层仿函数类似,有了lambda之后我们的代码会变得更简单一些,要实现一个add可以这样写

int main()
{

	auto add1 = [](int x, int y)->int
		{
			return x + y;
		};
	int ret = add1(10, 20);
	cout << ret;
	return 0;
}

刚才说了lambda的底层是仿函数,lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类
这个怎么验证我们可以结合这段代码把汇编打开看看

class add
{
public:
	add(int input)
		:_num(input)
	{}
	int operator()(int x, int y)
	{
		return x + _num +  y;
	}
private:
	int _num;
};
int main()
{
	int nums = 10;
	//函数对象
	add r1(nums);
	
	auto add1 = [nums](int x, int y)->int
		{
			return x + y + nums;
		};
	r1(10000, 2);
	add1(20000, 3);
    

	return 0;
}

在这里插入图片描述
我们可以发现他们的底层都是调用operator(),lambda本质就是编译器生成一个仿函数
在这里插入图片描述
每个lambda都有一个独一无二的名字

仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返
回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成
的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕
捉,编译器要看使⽤哪些就传那些对象

说完了原理,我们来讲解lambda的具体是怎么使用的和语法规则,我们先分析一下结构
在这里插入图片描述
注意
1、捕捉为空也不能省略
2、参数为空可以省略
3、返回值可以省略,可以通过返回对象⾃动推导
4、函数题不能省略

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接
收 lambda 对象。

每一个 lambda 表达式,编译器都会自动生成一个独一无二、无名字的内部仿函数类型

捕捉列表

为什么会有捕捉列表 lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉

第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,
y, &z] 表⽰x和y值捕捉,z引⽤捕捉。

	int x = 10;
	int y = 20;
	int z = 30;
	auto add1 = [x,y,z]()->int
		{
			return  x+y+z;
		};

第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表
写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些
变量。

//值捕捉
	int x = 10;
	int y = 20;
	int z = 30;
	auto add1 = [=]()->int
		{
			return  x+y+z;
		};
//引用捕捉
	int x = 10;
	int y = 20;
	int z = 30;
	auto add1 = [&]()->int
		{
			return  x+y+z;
		};
	int ret = add1();
	cout << ret;

第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,
x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是
&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必
须是引⽤捕捉。这里编译器会全部捕捉吗,当然不是是看你用那些就捕捉那些
捕捉的变量不能修改,引用的变量可以修改
在这里插入图片描述
在这里插入图片描述
如果是全局的或者静态的不需要捕捉直接用就行了

默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,
mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以
修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为
空)。
在这里插入图片描述
加了mutable之后
在这里插入图片描述
就可以修改了,但这里的x是拷贝过来的一个副本,和外面的x不是一个东西

包装器

function

std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标,他的底层实际上就是一个函数指针
我们来看看使用语法
在这里插入图片描述

class compare
{
public:
	bool operator()(int x, int y)
	{
		return x > y;
	}
};

int main()
{
	auto ret = [](int x, int y)->int {
		return x + y;
	};
	//包装可调用对象
	function<int(int, int)> add_func = add;
	cout << add_func(1, 2) << endl;
	function<bool(int, int)> compare1 = compare();
	cout << compare1(2, 3)<< endl;
	function<int(int,int)> add1 = [](int x, int y)->int {return x + y;};
	cout << add1(4,5) << endl;

	return 0;
}

下面说说包装成员函数,成员函数分静态成员函数和普通成员函数他们两个的包装方式是不一样的

class a
{
public:
	a(int n = 10)
		:_n(n)
	{}
	static int aa(int a, int b)
	{
		return a + b;
	}
	double ab(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	//包装静态成员函数
	function<int(int, int)> f = &a::aa;
	//包装普通成员函数
	function<double(a*, double, double)> f1 = &a::ab;
	return 0;
}

静态成员函数不属于实例对象,属于类本身,不需要对象指针,直接匹配 int(int,int)
包装普通成员函数时,我们需要注意到普通成员函数有一个隐藏的this指针参数比如(a* this),所以绑定的时候要传递对象或者对象的指针

&aa 就是传给成员函数的 this 指针;
后续传入两个 1.1;
a aa;
cout << f1(&aa, 1.1, 1.1) << endl;

bind

bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。bind 也在这个头⽂件中。这个bind本质是用来调整参数个数和顺序,把可调用对象传给他和参数传给他,他可以调整参数个数和顺序他是怎么调整的,这里他引入了一个占位符的概念就如同下面这样,下面这个就是一个占位符

 placeholders::_1;
 placeholders::_2;
 placeholders::_3;

我们来举一个例子

int sub(int x, int y)
{
	return x - y;
}
int main()
{       //_1始终代表第一个实参,_2始终代表第二个实参
	auto sub1 = bind(sub, _1, _2);
	cout << sub1(20,10) << endl;
	return 0;
}

我们可以来画图看看
大致的传参过程,但底层不是这样的
在这里插入图片描述

这样就相当于有了一个中间层,我们可以方便的调整参数顺序
在这里插入图片描述

调整参数顺序之后,结果就变成负数了
除了调整顺序还可以调整参数个数
这个在实践当中才是真正的使用场景
在这里插入图片描述
这里绑定参数是非常灵活的
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
除了绑定这写,我们还可以绑定一些成员函数,成员函数进行绑死,就不需要每次传递了
右边 bind 整体结构

bind(函数指针, 对象实例, 参数占位符)
&a::ab 是类 a 的成员函数地址。
成员函数必须依附一个对象才能调用,不能单独运行。
a() 表示临时构造一个 a 类的匿名对象,作为成员函数的调用者。

绑定返回的是一个仿函数对象,仿函数对象又可以直接交给function包装
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

我们可以用bind和function结合写一个计算复利的小程序

auto func1 = [](double rate, double money, int year)->double {
	double ret = money;
	for (int i = 0; i < year; i++)
	{
		ret += ret * rate;
	}
	return ret - money;
	};
// 绑死⼀些参数,实现出支持不同年华利率,不同⾦额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);

cout << func3_1_5(1000000) << endl;
//.......

这就是bind加function运用的一个很好的体现

更多推荐