【深度剖析 C++11】 第二弹: 现代C++ 的关键版本和核心驱动

 C++学习笔记:

C++ 进阶之路__Zwy@的博客-优快云博客

各位于晏,亦菲们,请点赞关注!

我的个人主页:

_Zwy@-优快云博客


目录

1、前言

2、类型分类

2.1、纯右值:(prvalue)

2.2、将亡值:(xvalue)

3、引用折叠

3.1 、引用折叠的规则

3.2、函数模板的引用折叠

3.2.1、左值引用模板和万能引用模板

3.2.2、func1的折叠

3.2.1、func2的折叠

4、完美转发

4.1、使用场景

4.2、forward

4.2.1、函数模板

4.2.2、返回值

4.2.3、使用方法

5、lambda

5.1、lambda表达式的基本语法

5.2、简单的lambda表达式

5.3、捕捉列表

5.3.1、显示捕捉

5.3.2、隐式捕捉

5.3.3、混合捕捉

5.4、注意

5.5、lambda的应用

5.6、lambda的原理

6、总结


1、前言

书接上回,我们主要讲了关于C++11的 列表初始化,右值引用和移动语义,以及类新增的移动构造和移动赋值等成员函数 大家简单复习下后 步入本文正题。

2、类型分类

在C++11后,进一步对类型进行了划分,右值被划分为纯右值(prvalue)和 将亡值 (xvalue)。C++98中的右值就等价于C++11中的纯右值。

泛左值(glvalue),泛左值包含将亡值(xvalue)和 左值(lvalue)。

2.1、纯右值:(prvalue)

纯右值是表达式求值过程中产生的临时值,或者用于初始化对象的字面量等。它们通常没有持久的存储,只在表达式求值期间存在。

2.2、将亡值:(xvalue)

将亡值是一种特殊的右值,它与对象的资源获取和转移相关。通常是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达。std::move 函数的返回值就是将亡值,move(x)。转换到对象的右值引用类型的类型转换表达式,如 static_cast<char&&>(x)。

总的来说,有变量名,就是泛左值;有变量名,且不能被move,就是左值;有变量名,且可以被move,就是将亡值;没有名字,且可以被移动,则是纯右值。不是左值和纯右值就是将亡值。

3、引用折叠

引用折叠是 C++ 中的一个重要概念,它主要出现在模板编程和类型推导的上下文中。在C++中如果我们想定义一个引用的引用,是不能直接定义的。但是通过模板或者typedef对类型重命名可以构成引用的引用。

3.1 、引用折叠的规则

typedef实现引用折叠

1、左值引用绑定到左值引用,折叠为左值引用。T& & 折叠为 T&

void Test()
{
	typedef int& lref;
	typedef int&& rref;
	int a = 10; //a的类型为int
	lref & b = a; //b的类型为int& & 折叠为int&
}

2、左值引用绑定到右值引用, 折叠为左值引用。T&  && 折叠为 T&

void Test()
{
	typedef int& lref;
	typedef int&& rref;
	int a = 10; //a的类型为int
	lref && b = a; //b的类型为int& && 折叠为int&
}

3、右值引用绑定右值引用 折叠为右值引用。  T&& && 折叠为 T&&

void Test()
{
	typedef int& lref;
	typedef int&& rref;
	int a = 10; //a的类型为int
	rref && b = move(a); //b的类型为int&& && 折叠为int&&
    rref && c=10; //c的类型也是 int&& && 折叠为 int&&
}

4、右值引用绑定左值引用 折叠为左值引用 。 T& && 折叠为 T&

void Test()
{
	typedef int& lref;
	typedef int&& rref;
	int a = 10;//a的类型为int
	rref & b = a; //b的类型为 int&& & 折叠为int&
}

3.2、函数模板的引用折叠

3.2.1、左值引用模板和万能引用模板

//由于引⽤折叠限定,func1实例化后只能是左值引⽤,左值引用模板
template<class T>
void func1(T& x)
{

}
// 由于引⽤折叠限定,func2实例化后可以是左值引⽤,也可以是右值引⽤,也叫万能引用模板
template<class T>
void func2(T&& x)
{

}

下面我们通过具体的例子验证。

3.2.2、func1的折叠

//由于引⽤折叠限定,func1实例化后只能是左值引⽤
template<class T>
void func1(T& x)
{

}

void Test()
{
	int n = 10;
	func1<int>(n); 
	//没有引用折叠 实例化为 void func1(int& x)
	func1<int&>(n); 
	//int& & 折叠为int& 实例化为 void func1(int& x)
	func1<int&&>(n); 
	//int& && 折叠为int& 实例化为 void func1(int& x)
	func1<const int&>(n); 
	//const int& & 折叠为 const int&  实例化为 void func1(const int& x)
	func1<const int&&>(n);
	//const int&& & 折叠为 const int&  实例化为 void func1(const int& x)
}

无论如何实例化 func1只能折叠为 左值引用或者const 左值引用

3.2.1、func2的折叠

// 由于引⽤折叠限定,func2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void func2(T&& x)
{

}
void Test()
{
	int n = 10;
	func2<int>(10); 
	//没有引用折叠 实例化为 void func1(int&& x)
	func2<int&>(n); 
	//int&& & 折叠为int& 实例化为 void func1(int& x)
	func2<int&&>(10); 
	//int&& && 折叠为int&& 实例化为 void func1(int&& x)
	func2<const int&>(n); 
	//const int&& & 折叠为 const int&  实例化为 void func1(const int& x)
	func2<const int&&>(10);
	//const int&& && 折叠为 const int&&  实例化为 void func1(const int&& x)
}

func2实例化后既可以折叠为左值引用,也可以右值引用

4、完美转发

4.1、使用场景

上节课我们讲过,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量在用于表达式时的属性是左值!!!(这是理解完美转发的关键),结合下面这个场景。

void func(int& x)
{
	cout << "void func(int& x)" << endl;
}

void func(int&& x)
{
	cout << "void func(int&& x)" << endl;
}
template<class T>
void Function(T&& x)
{
	//这里无论x是左值还是右值,传递给func时只会匹配左值引用的版本
    //因为右值变量在用于表达式时的属性是左值!!
	func(x);
}

void Test()
{
	int a = 10;
	//实例化为左值引用 传左值
	Function<int&>(a);
	
	//实例化为右值引用 传右值
	Function<int&&>(10);
}

int main()
{
	Test();
	return 0;
}
无论传递的是左值还是右值,传递给下一层函数func时都只会匹配到左值引用的版本,为了保持参数的原有属性,使其匹配到下一层函数对应的版本,我们就需要使用完美转发。

4.2、forward

完美转发forward本质是⼀个函数模板,主要还是通过引用折叠的方式实现。

4.2.1、函数模板

4.2.2、返回值

如果参数arg是左值引用(T&),那么不改变它的属性,该函数直接返回类型不变的arg。
否则,函数返回一个右值引用(T&&),该引用指向可用于传递右值的arg。

4.2.3、使用方法

还是上面那个例子我们结合完美转发就可以解决。

#include<utility>
void func(int& x)
{
	cout << "void func(int& x)" << endl;
}
void func(int&& x)
{
	cout << "void func(int&& x)" << endl;
}
template<class T>
void Function(T&& x)
{
	//这里我们将x使用forward完美转发,不改变它的属性,就可以使其匹配到对应的func版本
	func(forward<T>(x));
}
void Test()
{
	int a = 10;
	//实例化为左值引用 传左值
	Function<int&>(a);
	
	//实例化为右值引用 传右值
	Function<int&&>(10);
}

int main()
{
	Test();
	return 0;
}

左值匹配到左值引用版本,右值匹配到右值引用的版本。

5、lambda

5.1、lambda表达式的基本语法

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。 lambda表达式在使用层而言没有类型,我们一般使用auto来接收。

lambda表达式的格式:

[capture-list]  (parameters) -> return type { function boby }

[capture-list]:捕捉列表

该列表总是出现在 lambda 函数的开始位置,编译器根据[ ]来判断接下来的代码是否为 lambda 函数,关于捕捉列表我们下面详细讲解。

(parameters):参数列表。

与普通函数的参数列表相似,如果不需要参数,可以连同()一起省略。

-> return type:返回值类型。

用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{ function boby }:函数体。

与普通函数的函数体类似,函数体内除了可以使用参数以外,还可以使用捕捉列表所捕获的变量,函数体为空也不能省略。

使用:和普通函数一样使用。

5.2、简单的lambda表达式

void Test()
{
	// 1、捕捉为空也不能省略
	// 2、参数为空可以省略
	// 3、返回类型可以省略,可以通过返回对象⾃动推导
	// 4、函数体不能省略
	
	//一个简单的lambda表达式
	auto add = [](int x, int y)-> int {return x + y; };
	cout << add(1, 2) << endl;

	//也可以写成普通函数的形式
	auto print = [] //省略参数列表 省略返回类型 编译器自动推导
		{
			cout << "我是一个简单的lambda表达式" << endl;
		};
	print();

	int a = 0, b = 1;
	auto swap= [] (int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap(a, b);
	cout << a << ":" << b << endl;
}

5.3、捕捉列表

lambda 表达式中默认只能用 lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要通过捕捉列表进行捕捉。注意:全局变量不需要捕捉即可使用。

5.3.1、显示捕捉

显示捕捉分为传值捕捉和传引用捕捉。捕捉的多个变量中间使用逗号分隔,例如:[x , y,& z] 
x , y为传值捕捉,z为传引用捕捉。
//全局变量
int a = 15;

void Test()
{
	//显示捕捉
	int b= 5;
	int c = 10;
	auto func = [b, &c]
		{
			//传值捕捉的变量不可以修改
			//b = 10;
			//传引用捕捉的变量可以修改
			++c; 
			//全局变量可以修改
			++a;
		};
	func();
	cout << a <<" " << b <<" " << c;
}

5.3.2、隐式捕捉

在捕捉列表写⼀个=表示隐式传值捕捉,在捕捉列表写⼀个&表示隐式传引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。不需要我们显示的写。
//全局变量 捕捉到可以修改
int a = 10;

void Test()
{
	int b = 15;
	int c = 20;
	// 隐式值捕捉 不能修改 
 //用了哪些变量编译器自动捕捉
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};
	cout << func2() << endl;
	// 隐式引⽤捕捉 可以修改
	// 用了哪些变量编译器自动捕捉
	auto func3 = [&]
		{
			a++;
			b++;
			c++; };
	func3();
	cout << a << " " << b << " " << c << " " << endl;
}

5.3.3、混合捕捉

在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第⼀个元素必须是 &或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
void Test()
{
	int a = 10;
	int b = 15;
	int c = 20;
	int d = 25;

	// 混合捕捉1
	//b和c值捕捉,其他引用捕捉
	auto func1 = [&,b,c]
		{
			a++;
			//b++;
			//c++;
			d++;
			return a + b + c + d;
		};
	func1();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 混合捕捉2
	// a和b引用捕捉 其他值捕捉
	auto func2 = [=, &a, &b]
		{
			a++;
			b++;
			//c++;
			//d++;
			return a + b + c + d;
		};
	func2();
	cout << a << " " << b << " " << c << " " << d << endl;
}

5.4、注意

 lambda 表达式如果在函数局部域中,可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, 可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。 
//全局变量
int a = 1;
// 定义在全局的lambda捕捉列表必须为空,因为全局变量不⽤捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
	{
		a++;
	};
void Test()
{
	func1();
	//静态局部变量 也不需要捕捉
	static int b = 2;
	int c = 3;
	int d = 4;
	//定义在局部的lambda表达式可以捕捉局部变量,不能捕捉全局变量和静态局部变量
	auto func2 = [&c,d]()
		{
			++a;
			++c;
			++b;
			//++d c传值捕捉不能修改
			return a + b + c + d;
		};
	cout << func2() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;
}
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以 修改了,但是修改还是形参对象,不会影响实参。本质上是一种拷贝。使用该修饰符后,参数列表不可省略(即使参数为空)。
void Test()
{
	// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
	// mutable相当于去掉const属性,可以修改了
	// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷贝,修改形参不影响实参
	int a = 5;
	int b = 10;
	string s("helloworld");
	auto func = [=]()mutable //全部值捕捉
		{
			//传值捕捉也可以修改,但是不影响实参
			a = 10;
			b = 20;
			s[5] = 'x';
			cout <<"形参修改:"<< a << " " << b << " " << s << endl;
		};
	func();
	cout <<"实参不变"<< a << " " << b << " " << s << endl;

}

5.5、lambda的应用

在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,使用起来也不方便。仿函数要定义⼀个类,使用起来也不便捷。使用lambda 去定义可调用对象,既简单又方便。 lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,在智能指针那节课我们模拟实现shared_ptr就用到了lambda,lambda的使用场景还有很多,后面我们会经常接触到。

下面我们使用lambda模拟一下在购买商品时,对商品按照不同属性排序的场景。
//描述商品信息的类
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;
	}
};
void Test()
{
		vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙子", 2.2, 3
		}, { "菠萝", 1.5, 4 },{"蓝莓",5,10}};
		// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
		// 不同项的⽐较,需要写不同的仿函数或者函数指针,相对还是比较麻烦的,那么这里lambda就很好⽤了
		
		//通过仿函数控制排序逻辑
		sort(v.begin(), v.end(), ComparePriceLess());
		sort(v.begin(), v.end(), ComparePriceGreater());

		//通过lambda控制按照价格排序
		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;
			});

		//通过lambda控制按照评价排序
		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;
			});
}

5.6、lambda的原理

实际上经过编译器编译后,来到汇编层,根本就没有lambda这个东西,lambda的底层就是仿函数对象,也就是说我们写了一个lambda表达式后,编译器会自动生成一个对应的仿函数的类。底层仿函数的类名是编译器按⼀定规则生成的,保证不同的 lambda生成的类名不同,lambda参数、返回类型、函数体就是 仿函数operator()的参数、返回类型、函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传哪些对象构造。

分别写一个仿函数和lambda来计算利息。

分别写一个仿函数和lambda来计算利息。
//计算年利率的仿函数
class Rate
{
public:
	Rate(double rate)
		: _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};
void Test()
{
	double rate = 0.49;
	//仿函数对象
	Rate r1(rate);
	//lambda表达式
	auto r2 = [rate](double money, int year) {
		return money * rate * year;
		};
	r1(10000, 2);
	r2(10000, 2);
}

下面我们通过具体的汇编代码来验证:
lambda底层的仿函数类名: 编译器按照一定的规则生成,防止重复.
lambda调用本质就是调用底层编译器生成的仿函数的operator():

6、总结

本章重点是引用折叠和lambda表达式,语法比较复杂,lambda表达式是C++11中非常重要的部分,大家要结合例子自己手动敲一遍,理解lambda的语法、使用以及lambda的原理!

下篇文章,我们会讲解C++11剩余的所有内容,完成对C++11的收尾。

创作不易,希望大家点赞收藏支持一下,关注博主,为你带来更多优质内容!

评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一整颗红豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值