C++11 lambda表达式、function类模板、bind函数适配器


lambda表达式

当我们在写代码的时候如果经常需要用到一个公用的功能,为了方便维护,通常就会通过重载类的operator ()来将其写成仿函数(具有函数特性的类)来使用。

从使用的角度来讲,仿函数虽然好用,但是如果需要根据多种不同的情况设计多个函数的时候,这时候使用仿函数无疑是一种折磨,不仅仅代码量增加,而且还有大量的代码冗余。

例如下面这种情况
在我们使用购物网站的时候,通常都可以选择以不同的属性例如名字、价格、好评度、销量等来为商品进行排序

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

/*
如果我们要使用sort来对货物进行排序,就必须要提供给他一个比较的规则,
也就是用仿函数来实现一个大小判断,但是我们并不知道他要按照哪种方式进行比较,
也不知道他是按照>、<、>=、<=等方式进行排序,在属性很多的情况下,代码就会出现下面这样的大量冗余
*/
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

struct CompareNumGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._num > gr._num;
	}
};

struct CompareNameGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._name > gr._name;
	}
};

//..................等等等等

这样的代码不仅有上面的缺点,他的可读性也非常的差,简单的逻辑还好,倘若复杂的话接手代码的人一定心态爆炸

所以C++11引入了lambda表达式来解决这个问题

同样的代码,在引入了lambda表达式后代码的可读性就大大的提升了。

	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price > g2._price; });
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price < g2._price; });
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num > g2._num; });
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num < g2._num; });

lambda表达式的语法

lambda表达式书写格式:

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

lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来
的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

捕获列表:

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

注意事项:

  • a. 父作用域指包含lambda函数的语句块
  • b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编 译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  • d. 在块作用域以外的lambda函数捕捉列表必须为空。
  • e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  • f. lambda表达式之间不能相互赋值,即使看起来类型相同

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

例如使用lambda表达式来实现一个reverse

//因为lambda表达式本质是一个匿名函数,是他自己唯一的未命名类型,所以需要使用auto来进行类型推导
auto reverse = [](string& str){
	int begin = 0, end = str.size() - 1;
	while (begin < end)
	{
		std::swap(str[begin++], str[end--]);
	}
};

int main()
{
	//使用变量名调用即可
	string str = "HelloWorld";
	reverse(str);
	
	return 0;
}

lambda表达式的原理

下面来砍空lambda表达式在汇编下的调用过程
在这里插入图片描述
转入反汇编进行查看,当我们调用lambda表达式时,可以看到编译器在底层调用了lamber_uuid类(uuid为中间的编号)的operator(),这个做法是不是有点眼熟,这就是仿函数的做法。
下面与仿函数进行一个对比

struct Reverse
{
	void operator()(string& str)
	{
		int begin = 0, end = str.size() - 1;
		while (begin < end)
		{
			std::swap(str[begin++], str[end--]);
		}
	}
};
int main()
{
	string str = "HelloWorld";
	Reverse()(str);
	
	return 0;
}

在这里插入图片描述
无论是从使用上看,还是从底层的汇编代码来看,lambda表达式都与仿函数几乎一模一样,所以由此可以推断出lambda表达式的原理,当定义了lambda表达式之后,编译器按照仿函数的形式自动生成一个lamber_uuid类,并重载其中的operator(),当用户进行调用的时候会自动通过该类的匿名对象来调用,使得其看起来和普通的函数一样。


function模板

C++中可调用对象(如函数指针,仿函数,lambda表达式等)的虽然都有一个比较统一的操作形式,但是定义方法五花八门,这样就导致使用统一的方式保存可调用对象或者传递可调用对象时,会十分繁琐。C++11中提供了std::function和std::bind统一了可调用对象的各种操作。

例如

// 普通函数
int add(int a, int b){return a+b;} 

// lambda表达式
auto mod = [](int a, int b){ return a % b;}

// 仿函数
struct divide{
    int operator()(int denominator, int divisor){
        return denominator/divisor;
    }
};

上面的几种不同的可调用对象虽然类型不同,但是根据参数和返回值可以共享同一种调用形式int(int ,int)
通过function就可以统一其调用形式

std::function<int(int ,int)>  a = add; 
std::function<int(int ,int)>  b = mod ; 
std::function<int(int ,int)>  c = divide(); 

定义格式:std::function<返回值(参数列表)> 名字

  • std::function 是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
  • std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。

function与重载函数

在使用function的时候还有一个需要注意的点,就是我们不能将重载过的函数直接放入function对象中,因为会有二义性的问题
例如

int add(int x, int y);
double add(double x, double y);

map<string, function<int(int, int)>> map;
map.insert({"+", add});//此时无法判断是哪个add

//所以此时就不能直接使用函数名进行插入,可以通过使用函数指针来指向对应函数,再通过函数指针插入来消除二义性
int (*func)(int, int) = add;
map.insert({"+", func});
在这里插入代码片

bind函数适配器

对于那些只在少量地方使用的简单操作,使用lambda表达式时非常好的,但是如果使用的地方多了,就应该定义一个函数,而不是多次编写相同的lambda表达式。而库函数提供的模板参数又大多是以一个可调用的对象来进行接收,而如果需要将函数写出仿函数的形式,又增添了不少麻烦,所以C++11提供了一个通用的函数适配器bind,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。(其实就是创建一个对象,将提供的函数作为其operator()的重载)

std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个仿函数;
  • 只绑定部分参数,减少可调用对象传入的参数。

bind的语法如下

bind (callable, arg_list);
/*
	返回值为绑定完成的可调用对象
	参数callable为需要绑定的函数
	arg_list为参数列表
*/

下面以reverse举个例子

void reverse(string& str)
{
	int begin = 0, end = str.size() - 1;
	while (begin < end)
	{
		std::swap(str[begin++], str[end--]);
	}
}

int main()
{
	string str = "HelloWorld";
	
	auto func = bind(reverse,std::placeholders::_1);
	func(str);
	
	cout << str << endl;
	return 0;
}

在arg_list中通常还会包含形如_n的名字,这是位于中,std::placeholders::_n占位符,表示它们占据了传递给callable的第n个参数,_1为第一个,_2为第二个,以此类推。

例如我们可以利用占位符来固定住第2个参数,只让用户传第一个参数

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

int main()
{
	auto func = bind(add, std::placeholders::_1, 5);
	
	cout << func(4) << endl;//9
	return 0;
}

如果需要绑定成员函数的时候,因为成员函数不会被隐式转换为函数指针,还需要取地址,还需要其指明其来源的对象。
例如

struct Reverse
{
	void reverse(string& str)
	{
		int begin = 0, end = str.size() - 1;
		while (begin < end)
		{
			std::swap(str[begin++], str[end--]);
		}
	}
};

int main()
{
	Reverse r;
	auto func = bind(&Reverse::reverse, &r, std::placeholders::_1);

	string str = "helloworld";
	func(str);

	cout << str << endl;
	return 0;
}

当需要传递的参数为引用时

bind的那些不是占位符的参数会被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数希望以引用的方式传递,或是要绑定参数的类型无法拷贝。这时候就需要借助到库函数中的ref函数

ostream & print(ostream &os, const string& s, char c)
{
    os << s << c;
    return os;
}

int main()
{
	ostringstream os1;//ostringstream没有拷贝构造,所以不能传拷贝,只能传引用
	bind(print, ref(os1), _1, c)
}

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/108424729