C++11-(2)

(一)C++11新增功能

1.1 引用折叠

C++中不能直接定义引用的引用,如int& && r = i ;,但是可以间接定义引用的引用,一般情况可以用typedef或者using关键字来实现

int main()
{
	//int i = 0;
	//int&&& r = i; //直接定义引用的引用是错误的

	typedef int& lref;
	using rref = int&&;
	int n = 10;
	//间接引用
	lref& r1 = n;
	lref&& r2 = n;
	rref& r3 = n;
	rref&& r4 = 1;
	return 0;
}

上面的代码中,通过模板或者typedef中的类型操作可以构成引用的引用,C++11中把这样的场景叫作引用折叠,它的折叠逻辑是,右值引用的右值引用折叠成右值引用,有左值引用的组合均被折叠成左值引用,所以代码中r1的类型是int&,r2的类型是int&,r3的类型是int&,只有r4的类型是int&&

1.1.1 在模板中使用引用折叠的场景

左值引用模板:

template<class T>
void f1(T& t) 
{ }

int main()
{
	int i = 10;

	//没有折叠,实例化->f1(int& t);
	f1<int>(i);
	f1<int>(0); //报错

	//有折叠,实例化->f1(int& t);
	f1<int&>(i);
	f1<int&&>(0); //报错

	//有折叠,实例化->f1(int& t);
	f1<int&&>(i);
	f1<int&&>(0); //报错

	//有折叠,实例化->f1(const int& t);
	f1<const int&>(i);
	f1<const int&&>(0); //被const修饰的int& 可以引用左值,也可以引用右值
	return 0;
}

右值引用模板:

template<class T>
void f2(T&& t) //右值引用的模板
{ }

int main()
{
	int i = 10;

	//没有折叠,实例化->f2(int& t);
	f2<int>(i);
	f2<int>(0); //报错

	//有折叠,实例化->f2(int& t);
	f2<int&>(i);
	f2<int&>(0); //报错

	//有折叠,实例化->f2(int&& t);
	f2<int&&>(i); //报错
	f2<int&&>(0);

	//有折叠,实例化->f2(const int& t);
	f2<const int&>(i);
	f2<const int&>(0); //被const修饰的int& 可以引用左值,也可以引用右值

	return 0;
}

总结:第一个模板为左值引用,由于折叠限定,实例化出来的都是左值引用,第二个模板为右值引用,由于折叠限定,实例化出来的可以为左值引用,也可以为右值引用,即第二个模板通常被称为“万能引用”

1.1.2 引用折叠是如何实现的

证明如下:

template<class T>
void func(T&& t) //右值引用模板
{
	int a = 0;
	T x = a;
	x++;
	std::cout << &a << std::endl;
	std::cout << &x << std::endl;
}
int main()
{
	func(10);
	//10是右值,则推导出来的T是int,实例化->func(int&& t);
	//func函数中,x++,a与x的地址不相同

	int b = 0;
	func(b);
	//b是左值,则推导出来的T是int&,实例化->func(int& t);
	//func函数中,x是a的别名,x++相当于a++,两者地址相同
	
	func(std::move(b));
	//std::move(b)是右值,则推导出来的T是int,实例化->func(int&& t);
	//func函数中,x++,a与x的地址不相同

	const int a = 11;
	func(a);
	//a是左值,则推导出来的T是const int&,实例化->func(const int& t);
	// func函数中,x是a的别名,x被const修饰不能进行赋值操作,两者地址相同
	
	func(std::move(a));
	//std::move(a)是右值,则推导出来的T是const int,实例化->func(const int&& t);
	//func函数中,x被const修饰不能进行赋值操作,两者地址不相同
	return 0;
}

总结:当实参是左值时,编译器会自动推导模板参数的类型为int&,发生引用折叠,使得左值引用均折叠成左值引用,当实参是右值时,编译器会自动推导模板参数的类型为int,不发生引用折叠,就还是右值引用

1.2 完美转发

看下面代码:

void fun(int& t) { std::cout << "左值引用" << std::endl; };
void fun(const int& t) { std::cout << "const左值引用" << std::endl; };
void fun(int&& t) { std::cout << "右值引用" << std::endl; };
void fun(const int&& t) { std::cout << "const右值引用" << std::endl; };

template<class T>
void Func(T&& t)
{
	fun(t);
}

int main()
{
	Func(10);
	//右值,推导出来的T为int,实例化->Func(int&& t);

	int a = 11;
	Func(a);
	//左值,推导出来的T为int&,实例化->Func(int& t);
	Func(std::move(a));
	//右值,推导出来的T为int,实例化->Func(int&& t);

	const int b = 22;
	Func(b);
	//左值,推导出来的T为const int&,实例化->Func(const int& t);
	Func(std::move(b));
	//右值,推导出来的T为const int,实例化->Func(const int&& t);
	return 0;
}

该代码,通过万能模板调用Func函数,然后再调用fun函数,实现根据不同的值的属性调用到对应的fun函数,如,10是右值就调用对应fun函数的右值引用;a是左值就调用对应fun函数的左值引用,我们来看运行结果

在这里插入图片描述
结果未达到我们的预期,是右值的没有匹配到对应的右值函数,这是因为,右值引用的属性还是左值,所以就会出现全是左值的情况,为了解决以上这种问题,C++11库中实现了一个功能“完美转发”,forward,该功能就是保持原来的属性,实现是左值引用的就保持左值属性,右值引用的就保持右值属性,该接口的本质实际上还是类型强转,使用方法如下:

template<class T>
void Func(T&& t)
{
	//将原代码改成这样即可
	fun(std::forward<T>(t));
}

在这里插入图片描述

1.3 lambda表达式语法

1.3.1 定义

lambda表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。lambda表达式对于语法使用层面而言没有类型,所以一般是用auto或者函数模板参数定义的对象去接收lambda对象

lambda表达式的格式:capture-list->return type{function body}

  • capture-list:捕捉列表,该列表总是出现在lambda函数的开始,编译器根据捕捉列表来判断接下来的代码是否为lambda函数。捕捉列表能够捕捉上下文中的变量lambda函数使用,捕捉列表可以通过传值和传引用捕捉,捕捉列表为空也不能省略
  • parmeters:参数列表。不传参时可以连同()一同省略
  • return type:返回类型,用于追踪返回类型形式声明函数的返回值类型,没有返回值时,可以省略。一般返回值类型明确的情况下,也可以省略,由编译器对返回值类型进行推导。
  • function body:函数体

用法示例:

int main()
{
	auto add = [](int x, int y)->int {return x + y; };
	std::cout << add(1, 2) << std::endl;

	auto func = [] //参数列表和返回值省略
	{
		std::cout << "hello world" << std::endl;
	};

	return 0;
}

1.3.2 lambda的使用场景

在学习lambda之前,可调用对象只有函数指针和仿函数对象,但函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对来说也会比较麻烦。使用lambda去定义可调用对象,既简单又方便

看下面代码:

#include <vector>
#include <iostream>
#include <algorithm>
struct goods
{
	std::string _name; //商品名称
	double _price; //价格
	int _evaluate; //评估

	goods(const char* name,double price,int evaluate)
		:_name(name)
		,_price(price)
		,_evaluate(evaluate)
	{ }
};
struct comparePriceLess
{
	bool operator()(const goods& lg, const goods& rg)
	{
		return lg._price < rg._price;
	}
};
struct comparePriceGreater
{
	bool operator()(const goods& lg, const goods& rg)
	{
		return lg._price > rg._price;
	}
};
int main()
{
	std::vector<goods> list = { {"苹果",1.2,2},{"香蕉",1.5,3},{"葡萄",2.1,5} };
	//对商品按价格进行排序
	std::sort(list.begin(), list.end(), comparePriceLess());
	std::sort(list.begin(), list.end(), comparePriceGreater());
	return  0;
}

代码中可以看到,通过实现仿函数对象或函数指针来支持不同项的比较,相对来说还是比较麻烦的,要实现好多个我们所需要的仿函数,那么lambda就很好的解决了这一点

代码如下:

#include <vector>
#include <iostream>
#include <algorithm>
struct goods
{
	std::string _name; //商品名称
	double _price; //价格
	int _evaluate; //评估

	goods(const char* name,double price,int evaluate)
		:_name(name)
		,_price(price)
		,_evaluate(evaluate)
	{ }
};
int main()
{
	std::vector<goods> list = { {"苹果",1.2,2},{"香蕉",1.5,3},{"葡萄",2.1,5} };
	auto priceLess = [](const goods& lg, const goods& rg){return lg._price < rg._price; };
	//对价格进行排序
	std::sort(list.begin(), list.end(), priceLess);
	std::sort(list.begin(), list.end(), [](const goods& lg, const goods& rg){return lg._price > rg._price; });
	//对评估进行排序
	std::sort(list.begin(), list.end(), [](const goods& lg, const goods& rg){
		return lg._evaluate < rg._evaluate;
		});
	std::sort(list.begin(), list.end(), [](const goods& lg, const goods& rg) {
		return lg._evaluate > rg._evaluate;
		});
	return 0;
}

lambda本质上还是一个仿函数,该表达式其实就相当于一个“语法糖”

1.3.3 捕捉列表

lambda表达式中默认只能使用lambda函数体和参数中的变量,如果想使用外层作用域中的变量就需要进行捕捉

  • 捕捉的第一种方法:在捕捉列列表中显示的传值捕捉和引用捕捉捕捉的多个变量用逗号分割

伪代码例:

int a = 1,b = 2,c = 3;
auto add = [a,b,&c]{return a + b + c;}; 
//捕捉列表表示:a和b是值捕捉,&c是引用捕捉

注意:传引用捕捉的变量,若在lambda中被修改了,外部域的也会跟着修改

  • 捕捉的第二种方法:隐式捕捉,[=],表示隐式捕捉;[&],表示隐式引用捕捉,这样我们在lambda表达式中用了哪些变量,编译器就会自动捕捉

伪代码例:

int a = 1,b = 2,c = 3;
auto add = [=]{return a + b + c;}; 
auto func=[&]{a++;b--; return a+b+c;};

注意:lambda表达式中用了哪些变量,编译器就会去自动捕捉哪些变量,没有用到的就不会去捕捉

  • 捕捉的第三种方法:混合使用隐式捕捉和显示捕捉

伪代码例:

int a = 1,b = 2,c = 3,d = 4;
auto function1 = [=,&d]{d++;return a + d;}; //表示其他变量隐式捕捉,d显示引用捕捉
auto function2 = [&,a,b]{c++;return a + b;}; // 表示其他变量隐式引用捕捉,a和b是显示值捕捉

注意:当混合使用时,第一个元素必须是=或者&,并且第一个元素为&混合捕捉时,后面的捕捉必须为值捕捉,同理第一个元素为=混合捕捉时,后面的捕捉必须为引用捕捉

注意:局部的静态变量和全局变量不能捕捉,也不需要捕捉,直接就可以使用

1.3.4 mutable语法

lambda表达式中,传值捕捉本质上是一种拷贝,并且被const修饰所以不能被修改,但可以用mutable关键字来使值捕捉的变量在lambda表达式中被修改,但被修改的变量不会影响外部域的变量,因为它是一种拷贝

代码例:

int main()
{
	int a = 1, b = 2, c = 3;
	auto func = [=]()mutable {a++; b++; return a+b+c;};
	std::cout << func() << std::endl;
	std::cout << "a:" << a << " " << "b:" << b << " " << "c:" << c << std::endl;
	return 0;
}

在这里插入图片描述
可以看到外部域的变量并没有被修改

1.3.5 lambda的原理

lambda底层是仿函数对象,当写了一个lambda以后,编译器会自动生成一个对应的仿函数的类。仿函数的类型名是编译器按一定规则生成的,保证不同的lambda生成的类型名不同,lambda参数/返回类型/函数体就是仿函数operator的参数/返回类型/函数体,它的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,若是隐式捕捉,编译器要看使用哪些就传哪些变量.

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值