【C++详解】C++11(二) lambda表达式、类型分类、引⽤折叠、完美转发

发布于:2025-09-01 ⋅ 阅读:(16) ⋅ 点赞:(0)


一、lambda

lambda表达式语法

  • lambda 表达式本质是⼀个匿名函数对象,普通函数只能定义在全局,但是它可以定义在函数内部,并且它没有函数名。 lambda 表达式在语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
  • lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
  • [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来 判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda
    函数使⽤,捕捉列表可以传值和传引⽤捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。
  • (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
  • ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
  • {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
int main()
{
	// ⼀个简单的lambda表达式
	//[](int x, int y)->int {return x + y; } 整体是个对象
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;

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

	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;
}

捕捉列表

  • 首先明确lambda 表达式内部本身也属于一个局部域,lambda 表达式默认只能⽤ lambda 函数体中定义变量的和参数中的变量,如果想⽤外层作⽤域中的变量并且不想传参就需要进⾏捕捉。
  • 第⼀种捕捉⽅式是显示捕捉,在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z] 表⽰x和y值捕捉,z引⽤捕捉。(这里引用捕捉比较坑,注意和取地址区分开)
  • 值捕捉的变量不能修改,引⽤捕捉的变量可以修改,引⽤捕捉的变量在lambda 表达式内部修改会影响外部的被捕捉变量。
  • 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个&表⽰隐式引⽤捕捉,隐式值捕捉和隐式引⽤捕捉不能同时用。这样我们
    lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。
  • 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量式值捕捉,x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
  • lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,并且静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着
    lambda 表达式如果定义在全局位置,捕捉列表必须为空。(lambda 表达式一般都定义在局部,若需要定义在全局一般用仿函数)
  • 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
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;

	// 隐式值捕捉
	// ⽤了哪些变量就捕捉哪些变量
	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;

	// 混合捕捉1
	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;
}

lambda的应用

  • 在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤
    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;
	}
};
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;
}

lambda的原理

  • lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda
    以后,编译器会⽣成⼀个对应的仿函数的类。
  • 仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda
    的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda
    类构造函数的实参,当然如果是隐式捕捉,编译器要看使⽤哪些就传那些对象。
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);

	return 0;
}

二、类型分类

  • 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&&>(x)。
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

在这里插入图片描述

三、引⽤折叠

  • C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引⽤的引⽤。
  • 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,叫做引用折叠。这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
  • 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,模板类型没有让参数自行推导而是显示传递。小编每一个都写了详细注释,需要大家仔细理解⼀下。
// 由于引⽤折叠限定,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&
	//这里右值引用不能直接引用左值,引用左值需move左值
	rref&& r4 = 1; // r4 的类型是 int&&

	// 没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	//f1<int>(0); //报错,左值引用无法直接引用右值,需const左值引用

	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	//f1<int&>(0); //报错,左值引用无法直接引用右值,需const左值引用

	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	//f1<int&&>(0); //报错,左值引用无法直接引用右值,需const左值引用

	// 折叠->实例化为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;
}

  • 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤,只有编译器自动推导参数引用类型才是万能引⽤,显示传递参数引用类型不属于万能引⽤。
  • Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T编译器自动推导为int,实参是int左值,模板参数T编译器自动推导为int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function。

四、完美转发

在介绍完美转发之前小编引入一个场景,上一篇博客在介绍右值引⽤和移动语义在传参中的提效时借用了list的例子:

template<class T>
class list
{
public:
	void push_back(const T& x)
	{
		insert(end(), x);
	}

	//不是万能引用
	void push_back(T&& x)
	{
		insert(end(), move(x)); 
	}
};

我们学习了引用折叠之后小编想问一下这里void push_back(T&& x)是万能引用吗?其实这里不是万能引用,只属于引用折叠。因为我们前面介绍了万能引用的前提是模板参数自动推导,而这里模板参数T是类的模板参数,在创建类对象的时候就确定了,所以这里T参数不需要实参传递后模板参数自动推导。如果这里要实现一个万能引用代替之前的两个push_back的话就需要先给push_back加一个函数模板,参数传递后模板参数X自动推导类型,并且要用到我们接下来介绍的完美转发,因为这里push_back合二为一后参数x往下传递不能单纯的只传递x或者move(x),因为这样参数就只能单一性的传递右值或者左值,我们期望左值参数传递给push_back之后接着往下传递左值,右值参数传递给push_back之后接着往下传递右值,也就是保持调用push_back的实参对象属性,就需要用完美转发解决,示例如下:

template<class T>
class list
{
public:
	//void push_back(const T& x)
	//{
	//	insert(end(), x);
	//}

	////不是万能引用
	//void push_back(T&& x)
	//{
	//	insert(end(), move(x)); 
	//}

	//运用完美转发实现的万能引用
	template<class X>
	void push_back(X&& x)
	{
		insert(end(), forward<x> (x)));
	}
};
  • Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
  • 但是结合我们之前的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。
  • 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。
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(t); 
	//Fun(move(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;
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述