C++11:lambda表达式和包装器(function bind)

目录

1. lambda表达式

1.1 引入原因

1.2 lambda表达式

1.3 捕获列表 mutable关键字

1.4 lambda表达式原理

2. 包装器

2.1 function

2.2 bind


1. lambda表达式

1.1 引入原因

C++11之前,如果想要对一个数组排序,可以使用algorithm算法库中sort排序函数。一般情况下,排序的结果是升序的。因为默认使用less类中'()'运算符重载函数,此函数内的比较规则是返回小于比较的结果。

如果想要降序,需要改变元素比较规则,会使用到greater类。在sort函数传参时,传greater类匿名对象,就可以达到降序的效果

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void PrintVector(const vector<int> v)
{
	for (const auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	vector<int> v1= { 3,1,8,7,4,5,6,2,0,9 };
	// 默认按照小于<比较,排出来结果是升序
	sort(v1.begin(), v1.end());
	PrintVector(v1);

	vector<int> v2 = { 3,1,8,7,4,5,6,2,0,9 };
	// 如果需要降序,需要改变元素的比较规则,传greater仿函数匿名对象
	sort(v2.begin(), v2.end(), greater<int>());
	PrintVector(v2);

	return 0;
}

 运行结果如下:

如果待排序对象是自定义类型,需要用户定义排序比较的规则。如下面所示,定义一个学生类,包含姓名,年龄和成绩三个成员变量。如果想按照年龄排升序,得自己实现一个类重载()函数。如果想按照成绩排降序,也需要自己实现。

struct Student
{
	string _name; //姓名
	int _age;     //年龄
	float _grade; //成绩

	Student(string name, int age, float grade)
		:_name(name)
		,_age(age)
		,_grade(grade)
	{}
};

struct CompareAgeLess       //年龄升序
{
	bool operator()(const Student& s1, const Student& s2)
	{
		return s1._age < s2._age;
	}
};

struct CompareGradeGreater  //成绩降序
{
	bool operator()(const Student& s1, const Student& s2)
	{
		return s1._grade > s2._grade;
	}
};

void PrintVector(const vector<Student> v)
{
	for (auto& e : v)
	{
		cout << e._name << " ";
	}
	cout << endl;
}

int main()
{
	vector<Student> v = { {"张三", 22, 84.9}, {"李四", 20, 88.4}, {"王五", 23, 86.7} };

	sort(v.begin(), v.end(), CompareAgeLess());
	PrintVector(v);

	sort(v.begin(), v.end(), CompareGradeGreater());
	PrintVector(v);

	return 0;
}

运行结果如下:

 随着C++的发展,人们开始觉得上面的写法有点复杂。为了实现一个sort算法,需要重新写一个类。并且每次比较逻辑不同,需要实现多个类,带来了极大的不便。因此,C++11语法中引进了Lambda表达式

1.2 lambda表达式

lambda表示式的格式:[capature-list] (parameters) mutable -> return-type {statement}

  1. 捕获列表(capture list):该列表总是出现在lambda函数的开始位置,指定了lambda表达式可以访问哪些外部变量。
  2. 参数列表(parameters):与普通函数的参数列表相同,指定了lambda函数的参数。如果不需要参数传递,则可以连通()一起省略。
  3. mutable关键字:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略。
  4. 可返回类型(->return type):lambda表达式的返回类型,没有返回值时可以省略。返回值类型明确情况下,也可省略,编译器会根据返回值自动推断。
  5. 函数体({statement}):该函数体内,除了可以使用传递的参数外,还可以使用所捕获的变量。

我们先写几个简单的lambda函数。

  • 第一个是两数相加的函数,捕获列表我们暂时不用。除了函数名没写,其他函数的部分都存在。我们创建一个对象,使用auto自动推导类型,接收这个函数,就可以像函数一样调用它。
  • 第二个函数是打印几句话,因为不要传递参数,也没有返回值,可以省略掉“()->”。
  • 第三个函数有返回值,也可以省略返回值类型的部分,编译器可以自动推导。
int main()
{
	auto Add1 = [](int x, int y)->int { return x + y; };
	cout << Add1(10, 20) << endl;

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

	//可以进行自动推导
	auto Fun2 = []
	{
		cout << "hello world" << endl;
		cout << "hello world" << endl;
		cout << "hello world" << endl;
		return 0;
	};
	Fun2();

	return 0;
}

运行结果如下。不过还是建议将返回值类型写上,方便解析lambda函数。

练完手之后,我们试着使用lambda表达式作为sort排序规则的参数。其中lambda表达式可以直接写在sort参数列表中。像这种返回值类型比较明确的,lambda中的返回值省不省略都可以。

int main()
{
	vector<Student> v = { {"张三", 22, 84.9}, {"李四", 20, 88.4}, {"王五", 23, 86.7} };

	//年龄升序
	sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)->bool
		{
			return s1._age < s2._age;
		});
	PrintVector(v);

	//年龄降序
	sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)->bool
		{
			return s1._age > s2._age;
		});
	PrintVector(v);

	//成绩升序
	sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)
		{
			return s1._grade < s2._grade;
		});
	PrintVector(v);

	//成绩升序
	sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)
		{
			return s1._grade > s2._grade;
		});
	PrintVector(v);

	return 0;
}

运行结果如下:

1.3 捕获列表 mutable关键字

我们使用lambda写一个交换函数。如下面代码所示,可以写一个引用进行交换。但是lambda函数不仅可以使用传递的参数,也可以使用捕获列表中的对象。

swap2对象中,捕获两个整型变量m和n。如果你不加mutable关键字,无法改变捕获的参数。因为捕获的参数使用了const进行修饰。mutable有可变的意思,加上之后就可以修改了,需要注意此时不能省略参数列表。

int main()
{
	int m = 5, n = 10;

	auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	
	swap1(m, n);
	cout << "m->" << m << " " << "n->" << n << endl;
	
	auto swap2 = [m, n]()mutable
		{
			int tmp = m;
			m = n;
			n = tmp;
		};

	swap2();
	cout << "m->" << m << " " << "n->" << n << endl;

	return 0;
}

运行结果如下,你会发现调用swap2之后,m和n的值没有变回初始化的值。这是为什么呢?

因为这种捕捉是传值捕捉,传值捕捉本质是外面参数的一份拷贝,并且使用const修饰。使用mutable仅仅是去掉了const属性,可以修改这份拷贝,对于外面的参数没有影响。

如果想不传递参数交换两个数,可以使用传引用捕捉。用法是在捕获列表参数前加上&符号,捕获列表的参数就是外面参数的别名。不用加上mutable,因为引用捕捉默认不用const修饰。

int main()
{
	int m = 5, n = 10;

	auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	
	swap1(m, n);
	cout << "m->" << m << " " << "n->" << n << endl;

	auto swap2 = [&m, &n]()
		{
			int tmp = m;
			m = n;
			n = tmp;
		};

	swap2();
	cout << "m->" << m << " " << "n->" << n << endl;

	return 0;
}

运行结果如下:

如果局部域中的变量很多,写lambda表达式中一个个写到捕获列表中很麻烦。此时,就可以使用=和&符号,前者表示所有值传值捕捉,后者表示所有值传引用捕捉,在成员函数中包含this指针。

最后还可以混合捕捉,如下面代码中,a使用传引用捕捉,b使用传值捕捉,需要用逗号隔开。还可以所有值传引用捕捉,一个变量传值捕捉,或者是所有值传值捕捉,一个变量传引用捕捉。

int main()
{
	int a = 1, b = 2, c = 3, d = 4;
	// 所有值传值捕捉
	auto func1 = [=]
		{
			int ret = a + b + c + d;
			return ret;
		};
	cout << func1() << endl;
	cout << "a->" << a << " " << "b->" << b << " ";
	cout << "c->" << c << " " << "d->" << d << endl;

	// 所有值传引用捕捉
	auto func2 = [&]
		{
			a += 2;
			b--;
			c++;
			d /= 2;
			int ret = a + b + c + d;
			return ret;
		};
	cout << func2() << endl;
	cout << "a->" << a << " " << "b->" << b << " ";
	cout << "c->" << c << " " << "d->" << d << endl;

	//混合捕捉
	auto func3 = [&a , b]
		{
			a++;
			int ret = a + b;
			return ret;
		};
	cout << func3() << endl;
	cout << "a->" << a << " " << "b->" << b << " ";
	cout << "c->" << c << " " << "d->" << d << endl;

	auto func4 = [&, c]
		{
			a += 2;
			b--;
			d /= 2;
			int ret = a + b + c + d;
			return ret;
		};
	cout << func3() << endl;

	auto func5 = [=, &d]
		{
			d++;
			int ret = a + b + c + d;
			return ret;
		};
	cout << func3() << endl;

	return 0;
}

运行结果如下:

1.4 lambda表达式原理

下面代码中,先定义了名为Rate的类,重载了()函数,使用Rate对象r1,使用类似函数的操作,完成对利率的计算,实际上调用的就是()重载函数。第二块代码使用的是lambda表达式,捕捉rate变量,完成利率的计算。

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.03;

	// 函数对象
	Rate r1(rate);
	cout<< r1(10000, 2) <<endl;

	// lamber
	auto r2 = [rate](double money, int year)->double
		{
			return money * rate * year;
		};
	cout << r2(10000, 2) << endl;

	return 0;
}

运行结果如下,它们的操作看起来十分相似,得到的结果也是相同的。

我们调试该代码,进入汇编代码中。如下图,我们知道r1在初始化的时候调用了Rate的构造函数,然后使用类似函数的调用操作,实际上是调用了operator()函数。

  • 我们再来看lambda表达式的汇编代码,在用lambda表达式初始化r2时,会发现调用了<lambda_1>类的构造函数进行初始化,并且<lambda_1>在嵌套在许多类域之下。使用r2做类似函数调用的操作,在汇编代码发现也调用了<lambda_1>类的operator()函数
  • 说明lambda表达式写出的匿名函数是相对于我们而言的,编译器会在创建一个类,不同编译器对类取名规则不相同。并且每个lambda的生成的类名不同。
  • 初始化时,会从捕获列表中获取参数作为该类的成员变量,并定义operator()函数,方便做出类似函数调用的操作。

2. 包装器

2.1 function

function包装器,也是一种适配器。C++中,function本质是一个类模版,可以包装任意类型的可调用对象。可调用对象有函数指针仿函数(即定义了operator()函数的类)和lambda表达式

其中传递function类模版参数时,要类似于函数返回值和参数列表的形式,只留下返回值类型+(参数类型)。注意使用function需要包<functiona>l头文件

#include<functional>

int fun(int x, int y)
{
	return x + y;
}

struct Functor
{
	int operator()(int x, int y)
	{
		return x + y;
	}
};

int main()
{
    //函数指针
	function<int(int, int)> f1 = fun;
    //仿函数类对象
	function<int(int, int)> f2 = Functor();
    //lambda表达式
	function<int(int, int)> f1 = [](int x, int y) {return x + y; };

    cout << f1(3, 4) << endl;
	cout << f2(3, 4) << endl;
	cout << f3(3, 4) << endl;

	return 0;
}

运行结果如下:

包装上述对象正常操作即可,但是包装自定义类中的成员函数,有很多地方需要注意。

  • 包装静态成员函数时,因为受到类域的限制,需要手动指定类域。
  • 包装普通成员函数时,也要指定类域,并且函数名前要加上取地址符号,这是语法规定。普通成员函数的参数列表隐藏了this指针参数,需要匹配参数列表中的类型。有两种方法解决这个问题。
  • 第一种方法是function包装时,写上自定义类指针类型。在调用时,创建该类对象,传入对象的地址。
  • 第二种方法是在function传类参数上写上自定义类名。在调用时 ,可以直接传该类对象,或者是匿名对象。

为什么可以这样做呢?因为调用function类对象时,不是将参数直接传给成员函数,再实现函数调用。而是function拿到该函数的位置,并将其存储到function的成员变量中,在调用函数时,先调function中的operator(),再调该函数。所以不管是类对象指针,还是类对象,都可以调用到成员函数。

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};

int main()
{
	//静态成员函数,类中成员函数都要指定类域
	function<int(int, int)> f4 = Plus::plusi;

	//普通成员函数,需要加上取地址符号&
	function<int(Plus*, int, int)> f5 = &Plus::plusd;
	Plus pd;
	f5(&pd, 1.1, 1.1);

	//底层使用这个指针或者对象调用该函数
	function<int(Plus, int, int)> f6 = &Plus::plusd;
	f6(pd, 1.1, 1.1);
	f6(Plus(), 1.1, 1.1);

	return 0;
}

有function之后,可以建立一种变量跟包装器对象的映射关系。比如说一个数组中存储这一些字符串,里面有加减乘除的符号。如果不使用map容器建立字符串和函数的映射关系,那就要写switch判断或者是“if else“”语句,会十分麻烦。

int main()
{
	map<string, function<int(int, int)>> operatorMap =
	{
		{"+", [](int x, int y) {return x + y; }},
		{"-", [](int x, int y) {return x - y; }},
		{"*", [](int x, int y) {return x * y; }},
		{"/", [](int x, int y) {return x / y; }}
	};

	int a = 10, b = 4;
	vector<string> tokens = { "+", "$", "/", "&", "*" };

	for (auto& str : tokens)
	{
		if (operatorMap.count(str))
		{
			operatorMap[str](a, b);
		}
	}

	return 0;
}

2.2 bind

bind函数也是定义在functional头文件中,是一个模版函数。它是一个函数包装器,参数列表如上,先接受一个可调用对象,生成一个新的可调用对象。bind可以调整可调用对象参数的个数和参数的顺序。如果要使用bind,还得了解placeholders命名空间。

 

placeholder有占位符的意思,该命名空间中有许多的占位符,_1可以替代传入的可调用函数的第一个参数,_2可以替代传入的可调用函数的第二个参数,_N可以替代传入的可调用函数的第N个参数。

先定义一个三个参数的Func函数,做简单的运算。可以先用using声明前几个占位符,使用占位符时就不用展开类域了。下面是调整参数顺序和个数的代码。

  • 调整参数顺序,只需要交换占位符。比如交换Func函数中a和b参数传入的位置,只需要将bind中的_1和_2交换一下。
  • 调整参数个数一般是指固定某个参数。假如要固定Func第二个参数b,需要将bind中原先放第二参数的位置写上整型值,其他位置要写_1和_2,表示剩下的实参。
int Func(int a, int b, int c)
{
	int ret = (a - b) * c;
	return ret;
}

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main()
{
	//调整参数顺序-->不常用
	//按照Func参数顺序写,f1与Func调用情况一致
	auto f1 = bind(Func, _1, _2, _3);
	cout << f1(2, 1, 10) << endl;

	//改变Func函数中第一个参数和第二个参数
	auto f2 = bind(Func, _2, _1, _3);
	cout << f2(2, 1, 10) << endl;

	//调整参数个数-->常用
	//固定函数中第二个参数
	auto f3 = bind(Func, _1, 5, _2);
	cout << f3(10, 10) << endl;

	//固定函数中第一个参数
	auto f4 = bind(Func, 10,_1, _2);
	cout << f4(10, 10) << endl;

	return 0;
}

运行结果如下:

上面使用function包装普通成员函数时,第一个参数需要传类对象或者类对象指针,十分麻烦。可以使用bind绑定第一个参数,固定成匿名类对象,方便调用。

当然,绑定器可以绑定某个函数的许多参数,提供一个固定条件的接口,只需要传入一个参数即可使用。

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main()
{
	Plus pd;

	function<double(Plus*, double, double)> f1 = &Plus::plusd;
	cout << f1(&pd, 1.1, 1.1) << endl;

	function<double(Plus, double, double)> f2 = &Plus::plusd;
	cout << f2(Plus(), 2.2, 2.2) << endl;
	cout << f2(pd, 2.2, 2.2) << endl;

	//使用bind绑定第一个参数
	function<double(double, double)> f3 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f3(3.3, 3.3) << endl;

	return 0;
}

运行结果如下


创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

ee192b61bd234c87be9d198fb540140e.png

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值