【C++11】lambda表达式 | 函数包装器

一、lambda表达式

1. 为什么需要lambda表达式

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

#include <algorithm>
#include <functional>
int main()
{
	int array[] = {4,1,8,5,3,7,0,9,2,6};
	// 默认按照小于比较,排出来结果是升序
	std::sort(array, array+sizeof(array)/sizeof(array[0]));
	// 如果需要降序,需要改变元素的比较规则
	std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
	return 0;
}

如果待排序元素为自定义类型,需要用户使用仿函数定义排序时的比较规则:

struct Goods
{
	string _name;  //名字
	double _price; //价格
	int _num;      //数量
};

struct ComparePriceLess
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._price < g2._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._price > g2._price;
	}
};
struct CompareNumLess
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._num < g2._num;
	}
};
struct CompareNumGreater
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._num > g2._num;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
	sort(v.begin(), v.end(), ComparePriceLess());    //按价格升序排序
	sort(v.begin(), v.end(), ComparePriceGreater()); //按价格降序排序
	sort(v.begin(), v.end(), CompareNumLess());      //按数量升序排序
	sort(v.begin(), v.end(), CompareNumGreater());   //按数量降序排序
	return 0;
}

仿函数是重载了operator()的类,仿函数确实能够解决这里的问题,但可能仿函数的定义位置可能和使用仿函数的地方隔得比较远,这就要求仿函数的命名必须要通俗易懂,否则会降低代码的可读性。对于这种场景就比较适合使用lambda表达式。

2. lambda的定义

lambda表达式首先是一个可调用对象,是一个无名函数,一个lambda表达式可以被赋值给std::function对象,或者直接使用auto类型推导。std::function是一个通用的函数封装器,它可以包装任意可调用对象,包括函数指针、函数对象、成员函数指针以及lambda表达式。

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

lambda表达式提高了代码的可读性

3. lambda的语法

lambda表达式书写格式:

[capture-list] (parameters) mutable -> return_type { statement }

lambda表达式各部分说明:

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->return_type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。

因此C++11中最简单的lambda表达式为:[]{}; ,该lambda函数不能做任何事情。

捕捉列表

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用:

  • [var]:表示值传递方式捕捉变量var,默认情况下var在lambda作用域内是const,不可修改,除非lambda被mutable修饰。请添加图片描述

  • [=]:表示值传递方式捕获所有父作用域中的变量(成员函数包括this指针)。

  • [&var]:表示引用传递捕捉变量var。

  • [&]:表示引用传递捕捉所有父作用域中的变量(成员函数包括this指针)。

  • [this]:表示值传递方式捕捉当前的this指针。

注意:

  1. 父作用域指包含lambda函数的语句块
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:
    [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
    [&, a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
    比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  4. 在块作用域以外的lambda函数捕捉列表必须为空。
  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  6. lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)();
int main()
{
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };

	// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
	f1 = f2; // 编译失败--->提示找不到operator=()

	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();

	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();

	return 0;
}

请添加图片描述

4. 函数对象和lambda表达式的底层原理

函数对象,又称为仿函数,即可以像函数一样使用的对象,本质是在类中重载了operator()运算符的类对象。

C++中哪些是可调用对象?
可调用对象(Callable Objects)是指可以像函数一样被调用的实体。在C++中,有多种类型的可调用对象,包括以下几种:

  1. 函数指针(Function Pointers)
    • 普通函数指针,指向普通函数。
    • 成员函数指针,指向类的成员函数。
// 普通函数指针
int (*functionPointer)(int, int);

// 成员函数指针
class MyClass 
{
public:
	int memberFunction(int, int);
};

int (MyClass::*memberFunctionPointer)(int, int) = &MyClass::memberFunction;
  1. 函数对象(Function Objects)
    • 也称为仿函数,是一个重载了operator()的类对象,可以像函数一样被调用。
struct MyFunctor 
{
	int operator()(int a, int b) 
	{
		return a + b;
	}
};

MyFunctor myFunctor;
  1. Lambda 表达式
    • 匿名函数,可以在需要时直接定义和使用。
auto lambda = [](int a, int b) { return a + b; };
  1. std::function 对象
    • 是一个通用的函数封装器,可以包装任何可调用对象,包括函数指针、函数对象、成员函数指针以及 Lambda 表达式等。
#include<functional\>

		std::function<int(int, int)> myFunction = [](int a, int b) { return a + b; };
  1. 成员函数指针与对象绑定
    • 成员函数指针可以与特定的对象绑定,形成成员函数指针和对象的组合。
class MyClass 
{
public:
   int memberFunction(int, int);
};

MyClass myObject;
int (MyClass::*boundMemberFunctionPointer)(int, int) = &MyClass::memberFunction;

下面的代码编写了一个Add类,也编写了一个类似的lambda表达式:

class Add
{
public:
	Add(int base)
		:_base(base)
	{}
	int operator()(int num)
	{
		return _base + num;
	}
private:
	int _base;
};

int main()
{
	int base = 1;

	//函数对象
	Add add1(base);
	//下面两种调用方法是一样的
	add1.operator()(1000);
	add1(1000);

	//lambda表达式
	auto add2 = [base](int num)->int
		{
			return base + num;
		};
	add2(1000);
	return 0;
}

通过反汇编可以看到,实际编译器在底层对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的:请添加图片描述

如何理解lambda表达式之间不能相互赋值?
lambda表达式之间不能相互赋值,就算是两个一模一样的lambda表达式。

  • 因为lambda表达式底层的处理方式和仿函数是一样的,在VS2017下,lambda表达式在底层会被处理为函数对象,该函数对象对应的类名叫做<lambda_uuid>
  • 因此每个lambda表达式都有自己的唯一类型。这是因为lambda表达式在编译时被翻译成一个匿名的函数对象类,每个lambda表达式都会生成一个不同的类类型。。
  • 类名中的uuid叫做通用唯一识别码(Universally Unique Identifier),简单来说,uuid就是通过算法生成一串字符串,保证在当前程序当中每次生成的uuid都不会重复,当然VS2022处理的方式可能有所不同,但是目的是一样的:每个lambda底层对应的类名不同,保证每个lambda表达式底层类名都是唯一的。
    因此每个lambda表达式的类型都是不同的,这也就是lambda表达式之间不能相互赋值的原因,我们可以通过typeid(变量名).name()的方式来获取lambda表达式的类型。比如:
int main()
{
	int a = 10, b = 20;
	auto Swap1 = [](int& x, int& y)->void
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	auto Swap2 = [](int& x, int& y)->void
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	cout << typeid(Swap1).name() << endl;
	cout << typeid(Swap2).name() << endl;
	return 0;
}

对于上面的代码,在VS2022下,就算是两个一模一样的lambda表达式,它们的类型都是不同的:请添加图片描述

二、函数包装器

1. function包装器

function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

我们为什么需要function呢?下面是一个模板被实例化多份的问题:

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}

double f(double i)
{
	return i / 2;
}

struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lambda表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份:
请添加图片描述

请添加图片描述

包装器可以很好的解决上面的问题

std::function在头文件<functional>// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

模板参数说明:

  • Ret: 被调用函数的返回类型
  • Args…:被调用函数的形参

使用方法如下:

#include <functional>
int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	// 函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;

	// lambda表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)
		{return a + b; };
	cout << func3(1, 2) << endl;

	// 类的成员函数
	std::function<int(int, int)> func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;

	return 0;
}

有了包装器,如何解决模板的效率低下,实例化多份的问题呢?

#include <functional>
template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lambda表达式
	std::function<double(double)> func3 = [](double d)->double { return d / 4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}

请添加图片描述

2. bind包装器

bind也是一种函数包装器,也叫做适配器。它可以接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,C++中的bind本质是一个函数模板

调用bind的一般形式为:

auto newCallable = bind(callable, arg_list);

说明:

  • callable:需要包装的可调用对象。
  • newCallable:生成的新的可调用对象。
  • arg_list:逗号分隔的参数列表,对应给定的callable的参数。当调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

用bind包装器绑定固定参数

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//绑定固定参数
	function<int(int)> func = bind(Plus, placeholders::_1, 10);
	cout << func(2) << endl; //12
	return 0;
}

用bind包装器调整传参顺序

对于下面Sub类中的sub成员函数,sub成员函数的第一个参数是隐藏的this指针,如果想要在调用sub成员函数时不用对象进行调用,那么可以将sub成员函数的第一个参数固定绑定为一个Sub对象。比如:

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
	//绑定固定参数
	function<int(int, int)> func = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
	cout << func(1, 2) << endl; //-1
	return 0;
}

此时调用绑定后生成的可调用对象时,就只需要传入用于相减的两个参数了,因为在调用时会固定帮我们传入一个匿名对象给this指针。

如果想要将sub成员函数用于相减的两个参数的顺序交换,那么直接在绑定时将placeholders::_1和placeholders::_2的位置交换一下就行了。比如:

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
	//调整传参顺序
	function<int(int, int)> func = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
	cout << func(1, 2) << endl; //1
	return 0;
}

根本原因就是因为,后续调用新生成的可调用对象时,传入的第一个参数会传给placeholders::_1,传入的第二个参数会传给placeholders::_2,因此可以在绑定时通过控制placeholders::_n的位置,来控制第n个参数的传递位置。

无意义的绑定

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//无意义的绑定
	function<int(int, int)> func = bind(Plus, placeholders::_1, placeholders::_2);
	cout << func(1, 2) << endl; //3
	return 0;
}

3. bind包装器的意义

  • 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
  • 可以对函数参数的顺序进行灵活调整。
  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_宁清

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

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

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

打赏作者

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

抵扣说明:

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

余额充值