【C++】第二十六节—C++11(中) | 右值引用和移动语义(续集)+lambda

Hi ,我是云边有个稻草人,C++领域博主与你分享专业知识(*^▽^*)

《C++》本篇文章所属专栏---持续更新中---欢迎订阅~

目录

上节总览,详情见--->【C++】第二十五节---C++11 (上) | 详解列表初始化+右值引用和移动语义

本节总览

(4)右值引用和移动语义在传参中的提效

[6. 类型分类](#6. 类型分类)

[7. 引用折叠](#7. 引用折叠)

[8. 完美转发](#8. 完美转发)

四、lambda

[1. lambda表达式语法](#1. lambda表达式语法)

[2. lambda的应用](#2. lambda的应用)

[3. 捕捉列表](#3. 捕捉列表)

[4. lambda的原理](#4. lambda的原理)


接着上节,正文开始------

(4)右值引用和移动语义在传参中的提效
  • 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
  • 把我们之前模拟实现的list拷贝过来,支持右值引用参数版本的push_back和insert
  • 其实这里还有一个emplace系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲 解以后再讲解emplace系列的接口。
cpp 复制代码
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);

int main()
{
	std::list<bit::string> lt;
	bit::string s1("111111111111111111111");

	lt.push_back(s1);
	cout << "*************************" << endl;

	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;

	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************" << endl;

	return 0;
}

//可以自己分析一下运行结果
运行结果:
string(char* str)
string(const string& s) -- 拷贝构造
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(string&& s) -- 移动构造
*************************
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构

下面是自己实现 list 的右值版本的push_back。注意右值在层层传递的时候属性的变化,要move保持其右值属性才能调用移动构造

cpp 复制代码
// List.h
// 以下代码把跟这里无关的接口都删除了,精简版
namespace bit
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(const T& data = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(data)
		{
		}
		ListNode(T&& data)//这里不需要给缺省值,有缺省值的构造函数是默认构造函数,一个类里面只能有一个默认构造函数
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{
		}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		//左值版本
		void push_back(const T& x)
		{
			insert(end(), x);
		}

		//右值版本
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		iterator insert(iterator pos, T && x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(move(x));
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}

	private:
		Node* _head;
	};
}

// Test.cpp
#include"List.h"
int main()
{
	bit::list<bit::string> lt;
	cout << "*************************" << endl;

	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;

	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;

	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************" << endl;

	return 0;
}
6. 类型分类
  • C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、 true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。
  • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如 move(x)、static_cast(x)
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
  • 值替换---cppreference.comValue categories这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节。
7. 引用折叠
  • C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用(也就是说,只要存在左值引用就是左值引用)
  • 下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家需要一个一 个仔细理解一下。
  • 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
  • Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模 板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的Function。
cpp 复制代码
// 由于引用折叠限定,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;
}

对上面的代码进行分析:

cpp 复制代码
//万能引用
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;
	// b是左值,推导出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;
}
8. 完美转发
  • Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
  • 但是结合前面的讲解,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中 t 的属性是左值,那么我们把 t 传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发实现。
  • template T&& forward (typename remove_reference::type& arg);
  • template T&& forward (typename remove_reference::type&& arg);
  • 完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
cpp 复制代码
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
	return static_cast<_Ty&&>(_Arg);
}

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<class T>
void Function(T&& t)
{
	Fun(forward<T>(t));
}

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(b); // const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(std::move(b)); // const 右值

	return 0;
}

四、lambda

1. lambda表达式语法
  • lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。 lambda 表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收 lambda 对象。
  • lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
  • capture-list\] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据\[\]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,具体细节我们后面再细讲。捕捉列表为空也不能省略。

  • ->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导(建议写上,可读性更强,随你)。
  • {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
cpp 复制代码
int main()
{
	// 一个简单的lambda表达式
	auto add1 = [](int x, int y)->int {return x + y; };
	auto add1 = [](int x, int y){return x + y; };
	cout << add1(1, 2) << endl;

	// 1、捕捉为空也不能省略
	// 2、参数为空可以省略
	// 3、返回值可以省略,可以通过返回对象自动推导
	// 4、函数体不能省略
	auto func1 = []
	{
		cout << "hello" << endl;
		return 0;
	};

	func1();

	int a = 0, b = 1;
	auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};

	swap1(a, b);
	cout << a << ":" << b << endl;

	return 0;
}
2. lambda的应用
  • 在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的 类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对 象,既简单又方便。
  • lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定 制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到
cpp 复制代码
struct Goods
{
	string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价
	// ...
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{
	}
};

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };

	// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
	// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
	sort(v.begin(), v.end(), ComparePriceLess());

	sort(v.begin(), v.end(), ComparePriceGreater());

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate < g2._evaluate;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate > g2._evaluate;
		});

	return 0;
}
3. 捕捉列表
  • lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
  • 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x, y, &z] 表示x和y值捕捉,z引用捕捉。
  • 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
  • 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉, x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是 &或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
  • lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态 局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
  • 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
cpp 复制代码
int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
	{
		x++;
	};

int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
		{
			// 值捕捉的变量不能修改,引用捕捉的变量可以修改
			//a++;
			b++;
			int ret = a + b;
			return ret;
		};
	cout << func1() << endl;

	// 隐式值捕捉
	// 用了哪些变量就捕捉哪些变量,没有使用d就不会捕捉d
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};
	cout << func2() << endl;

	// 隐式引用捕捉
	// 用了哪些变量就捕捉哪些变量
	auto func3 = [&]
		{
			a++;
			c++;
			d++;
		};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 混合捕捉1
	auto func4 = [&, a, b]
		{
			//a++;
			//b++;
			c++;
			d++;
			return a + b + c + d;
		};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 混合捕捉2
	auto func5 = [=, &a, &b]
		{
			a++;
			b++;
			/*c++;
			d++;*/
			return a + b + c + d;
		};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 局部的静态和全局变量不能捕捉,也不需要捕捉
	static int m = 0;
	auto func6 = []
		{
			int ret = x + m;
			return ret;
		};

	// 传值捕捉本质是一种拷贝,并且被const修饰了
	// mutable相当于去掉const属性,可以修改了
	// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝
	auto func7 = [=]()mutable
		{
			a++;
			b++;
			c++;
			d++;
			return a + b + c + d;
		};
	cout << func7() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}
4. lambda的原理
  • lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for 这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
  • 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返 回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成 的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕 捉,编译器要看使用哪些就传那些对象。
  • 上面的原理,我们可以透过汇编层了解一下,下面第二段汇编层代码印证了上面的原理。
cpp 复制代码
class Rate
{
public:
	Rate(double rate)
		: _rate(rate)
	{
	}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};

int main()
{
	double rate = 0.49;

	// lambda
	auto r2 = [rate](double money, int year) {
		return money * rate * year;
		};

	// 函数对象
	Rate r1(rate);
	r1(10000, 2);
	r2(10000, 2);

	auto func1 = [] {
		cout << "hello world" << endl;
		};
	func1();

	return 0;
}

// lambda
auto r2 = [rate](double money, int year) {
	return money * rate * year;
	};

// 捕捉列表的rate,可以看到作为lambda_1类构造函数的参数传递了,这样要拿去初始化成员变量
// 下面operator()中才能使用
00D8295C lea eax, [rate]
00D8295F push eax
00D82960 lea ecx, [r2]
00D82963 call `main'::`2': : <lambda_1>::<lambda_1> (0D81F80h)

// 函数对象
		Rate r1(rate);
00D82968 sub esp, 8
00D8296B movsd xmm0, mmword ptr[rate]
00D82970 movsd mmword ptr[esp], xmm0
00D82975 lea ecx, [r1]
00D82978 call Rate::Rate(0D81438h)
		r1(10000, 2);
00D8297D push 2
00D8297F sub esp, 8
00D82982 movsd xmm0, mmword ptr[__real@40c3880000000000(0D89B50h)]
00D8298A movsd mmword ptr[esp], xmm0
00D8298F lea ecx, [r1]
00D82992 call Rate::operator() (0D81212h)

// 汇编层可以看到r2 lambda对象调用本质还是调用operator(),类型是lambda_1,这个类型名
// 的规则是编译器自己定制的,保证不同的lambda不冲突
		r2(10000, 2);
00D82999 push 2
00D8299B sub esp, 8
00D8299E movsd xmm0, mmword ptr[__real@40c3880000000000(0D89B50h)]
00D829A6 movsd mmword ptr[esp], xmm0
00D829AB lea ecx, [r2]
00D829AE call `main'::`2': : <lambda_1>::operator() (0D824C0h)

下节继续,本系列倒计时

完------


说了再见_周杰伦

......

至此结束------

我是云边有个稻草人

期待与你的下一次相遇... ...