C/C++教程

C++深入学习笔记(18)—— 定制操作(lambda表达式)

本文主要是介绍C++深入学习笔记(18)—— 定制操作(lambda表达式),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的>或==运算符来比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。

例如,我们知道sort算法默认的是按照<运算符来进行排序的,但是可能我们希望的排序顺序与<所定义的顺序不同,或者干脆我们就想让它是按照>运算符来进行排序的,在这些情况下,都需要重载sort的默认行为。

我们就以sort算法为例,假如我们需要让它进行降序排序,那么它将接受第三个参数,该参数称为谓词

谓词

谓词是一个可以调用的表达式,其返回结果是一个能够作条件的值。标准库算法所使用的谓词分为两类:一元谓词二元谓词。一元谓词只接受一个参数,同样的,二元谓词只接受两个参数。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。

我们让sort算法让序列变为降序排列。接受一个二元谓词参数的sort版本用一个谓词来代替<来比较元素。我们提供的谓词的操作必须在输入序列中所有可能的元素值上定义一个一致的序。由于<运算符返回一个bool类型的值,所以我们对序的定义也应当返回一个布尔值。

bool isLengther(const string &s1, const string &s2)
{
	return s1.length() > s2.length();
}

// vector<string> words = {"ab", "cde", "fghi"}
// 按照降序排列words
sort(words.begin(), words.end(), isLengther);
// words = {"fghi", "cde", "ab"}

lambda表达式

根据算法接受一个一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或者两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。这时,我们就需要用lambda表达式了

我们可以向一个算法传递任何类别的可调用对象。对于一个对象或者一个表达式,如果可以对其使用调用运算符(调用运算符是一对圆括号,里面放置实参列表。),则称它为可调用的。

一般来说,我们使用的可调用对象有两种:函数和函数指针。还有其它两种可调用的对象:重载了函数调用运算符的类以及lambda表达式

一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但是与函数不同,lambda可能定义在函数内部。
lambda表达式的形式:

[capture-list] (parameter-list) -> return-type { function-body}
capture-list:捕获列表
parameter-list:参数列表
return-type:返回类型
function-body:函数体

注意: 与普通函数不同的是,lambda必须使用尾置返回来制定返回类型

我们可以忽略参数列表和返回类型,但是必须永远包含捕获列表和函数体。

// 定义一个可调用对象f,它不接受参数,返回布尔值true
auto f = [] { return true;};
// lambda调用方式和普通函数调用方式一样,都用调用运算符即可
if(f())
	cout << "success" << endl;
else
	cout << "fail" << endl;
如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void

下面我们演示带参数的lambda表达式,实现和isLengther函数完全相同功能的lambda

[] (const string &s1, const string &s2) 
{ return s1.length() > s2.length();}
捕获列表为空表明此lambda不使用它所在函数中的任何局部变量。

那么捕获列表到底是什么呢?该如何去使用?
假设我们要求一个字符串序列,判断其中的字符串长度是否大于传入的参数sz,对其进行排序

int f(vector<int> &words, vector<int>::size_type sz)
{
	sort(words.begin(), words.end(), 
	[sz] (const string &s)
		{
			return size.length() > sz;
		})
}									

虽然一个lambda可以出现在一个函数中,使用其局部变量,但是它只能使用那些明确指明的局部变量。一个lambda通过将局部变量包含在其捕获列表中来指出它将会使用哪些变量。
捕获列表指引lambda在其内部包含访问局部变量所需要的信息。

由于此lambda捕获了sz,因此lambda可以使用sz。lambda没有捕获words,因此不能访问。

一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能再函数体
中使用该变量

lambda的捕获

类似于函数参数传递,变量的捕获方式也可以是值或者引用。
值捕获
与按值传参类似,采用值捕获的前提是变量可以拷贝。然而不同的是,被捕获的变量时在创建lambda时拷贝,而不是调用时拷贝。

void function()
{
	int i = 1;
	auto f = [i] () mutable { return ++i;};
	cout << f() << endl;		// 2
	cout << i << endl;		// 1
	i = 0;
	cout << f() << endl;		// 2,因为f保存了我们创建他时i的拷贝
}

由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。

引用捕获
与按引用传参类似。

void function()
{
	int i = 1;
	auto f = [&i] () mutable { return ++i;};
	cout << i << endl;		// 1
	cout << f() << endl;		// 2
	cout << i << endl;		// 2
	i = 0;
	cout << f() << endl;		//1
	cout << i << endl;		//1
当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。

隐式捕获
除了显示列出我们希望使用的来自所在函数的变量外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或者=。&告诉编译器采用引用捕获的方式,=啧表示采用值捕获的方式。

void function()
{
	// 值捕获
	int i = 1;
	auto f = [=] () mutable { return ++i;};
	cout << f() << endl;		// 2
	cout << i << endl;		// 1
	i = 0;
	cout << f() << endl;		// 2,因为f保存了我们创建他时i的拷贝
}
void function()
{
	// 引用捕获
	int i = 1;
	auto f = [&] () mutable { return ++i;};
	cout << i << endl;		// 1
	cout << f() << endl;		// 2
	cout << i << endl;		// 2
	i = 0;
	cout << f() << endl;		//1
	cout << i << endl;		//1

如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显示捕获。那么该怎么做呢?

void add(int x, int y)
{
	auto f = [&, y] () mutable { return (++x) + (++y);};
	cout << f() << " ";
	cout << x << " ";
	cout << y << " ";
}

add(1, 1);
// 结果为4 2 1

当我们混合使用隐式捕获和显示捕获时,捕获列表中的第一个元素必须是一个&或者=。此符号制定了默认捕获方式为引用或者值。用另一种方式捕获的变量必须显示的表示出来。

[ ]空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量后才能使用它们。
[name]name是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都在lambda创建时被拷贝。名字前如果加了&,则采用引用捕获的方式。
[&]隐式捕获列表,采用引用捕获的方式。lambda体中所使用的来自所在函数的变量都采用引用捕获的方式。
[=]隐式捕获列表,采用值捕获的方式,lambda体中所使用的来自所在函数的变量都采用值捕获的方式。
[&,identifier_list]identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获的方式,而任何隐式捕获的变量都采用引用捕获的方式。identifier_list中的名字前面不能使用&。
[=,identifier_list]identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用引用捕获的方式,而任何隐式捕获的变量都采用值捕获的方式。identifier_list中的名字前面必须使用&。

可变lambda

可以注意到,我上面的代码在定义lambda时哪怕参数列表为空,我也没有省略,并且在采纳数列表后面加上看一个关键字mutable。这是为什么呢?可以注意到,我的lambda都改变的有捕获到的变量的值。那么,这里就有一个可变lambda的概念。
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能够改变一个被捕获的变量的值,就必须在参数列表之后加上关键字mutable。因此,可变lambda不能省略参数列表。
一个引用捕获的变量是否可以修改依赖于此引用指向的是一个const类型还是一个非const类型。

指定lambda返回类型

到目前为止,我们所编写的lambda都只包含单一的return语句,默认返回返回的值的类型。其它情况下,如果没有制定返回类型,则默认返回void,被推断返回void的lambda不能返回值。
下面我们以标准库transform算法和一个lambda表达式来将一个序列的每一个负数替换为其绝对值。

// 正确写法
transform(vi.begin(), vi.end(),vi.begin(),
			[] (int i) { return i < 0? -i : i;});

// 错误写法
transform(vi.begin(), vi.end(), vi.begin(),
			[] (int i)
			{
				if(i < 0)
					return -i;
				else
					return i;
			});

为什么这个写法是错误的呢,我们只不过吧三元运算符等加成了一条if-else语句而已。我在上面已经说过了。

如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则
返回void

if-else不是单一的return语句,编译器推断错误版本的lambda返回值为void,但是它却返回了一个int值。

当我们需要为lambda定义返回值时,必须使用尾置返回类型

// 正确代码
transform(vi.begin(), vi.end(), vi.begin(),
			[] (int i) -> int
			{
				if(i < 0)
					return -i;
				else
					return i;
			});

参数绑定

对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。

如果lambda的捕获列表为空,通常可以用函数来替代它,但是对于捕获局部变量的lambda,用函数来替换它就不熟那么容易了。

// 有一个vector<string>类型的vs
find_if(vs.begin(), vs.end(),
	[sz] (const string &s) -> bool { return s.length() >= sz;});

我们可以编写一个可以同样完成lambda工作的函数

bool check_size(const string &s, string::size_type sz)
{
	return s.length() >= sz;
}

但是,我们不能将这个函数作为find_if的一个参数。因为find_if只能接受一个一元谓词,因此传递给find_if的可调用对象必须接受单一参数。lambda用了捕获列表来保存sz,那么为了用check_size来代替lambda,如何解决想sz形参传递一个参数的问题呢?

答案在这里,我们用一个标准库里的bind函数来解决该问题。该函数定义在functional头文件中。可以将bind函数看做一个通用的函数适配器,它接受一个可调用的对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用形式: auto newCallable = bind(callable, arg_list);

newCallable:新的调用对象
callable:原调用对象
arg_list:逗号分隔的参数列表,对应给定的callable参数。

即当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list参数。

占位符
arg_list中的参数可能包含形如_n的名字,其中n是一个正整数。这些参数是“占位符” 表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生辰递归可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

话不多说,上代码演示!

auto check6 = bind(check_size, _1, 6);
string s = "Hello World!";
bool b = check6(s);		// true

此时bind只有一个占位符,表示check6只接受单一参数。占位符出现在arg_list第一个位置,表示check6的此参数对于check_size的第一个参数。此参数是一个const string类型的引用。因此,调用check6必须传递给它一个string类型的参数,check6会将此参数传递给check_size。

// 使用bind函数用check_size替换原来基于lambda的find_if
find_if(vs.begin(), vs.end(), bind(cheak_size, _1, sz));

此时bind调用生成一个可调用对象,将check_size的第二个参数绑定到sz的值,当find_if对vs中的string调用这个对象时,这些对象会调用check_size,将给定的string和sz传递给它。因此,find_if可以有效地对输入的序列中每个string调用check_size。

placeholders
名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。为了使用这些名字,两个命名空间都要写上。对于_1的using声明为

// 此定义说明我们要使用的名字_1定义在命名空间placeholders中,而此命名
// 空间又定义在命名空间std中
using std::placeholders::_1;

然而,这种方式对于每一个占位符名字,我们都必须提供一个单独的using声明,太过于麻烦,也容易出错,因此我们可以用另一种using语句

// 以下两种均可
using namespace namespace_name;
using namespace std::placeholders;

bind的参数
我们可以用bind绑定给定可调用对象中的参数或者重新安排其顺序。例如,假定f是一个可调用对象,它有5个参数。

// g是一个有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, _1);

这个心的可调用对象将它自己的参数作为第三个和第五个参数传递给f。f的第一个、第二个和第四个参数分别被绑定到给定的值a、b、c上。

传递给g的参数按位置绑定到占位符。即,第一个参数绑定到_1,第二个参数绑定到_2。因此,当我们调用g时,其第一个参数将传递给f作为最后一个参数,第二个参数江北传递给f作为第三个参数。实际上,这个bind调用会将g(_1, _2)映射为f(a, b, _2, c, _1).

既然我们可以给绑定的可调用对象重新安排其顺序,那么是不是我们是不是可以在不改变原可调用对象的基础上改变其排序的顺序呢?

bool isLengther(const string &s1, const string &s2)
{
	return s1.length() > s2.length();
}

// vector<string> words = {"ab", "cde", "fghi"}
// 按照降序排列words
sort(words.begin(), words.end(), bind(isLengther, _1, _2);
// words = {"fghi", "cde", "ab"}

// 按照升序排列words
sort(words.begin(), words.end(), bind(isLengther, _2, _1);
// words = {"ab", "cde", "fghi"}

绑定引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数我们希望以引用的方式传递,或者要绑定参数的类型无法拷贝,这时我们该怎么做?

// 引用方式捕捉ostream的lambda
for_each(words.begin(), words.end(),
	[&os, c](const string &s) { os << s << c;});

我们可以编写一个和lambda一样功能的函数

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

但是,不能直接用bind来代替对os的捕获

// 错误,ostream对象是不能够拷贝的
for_each(words.begin(), words.end(), bind(print, os, _1, ' '));

// 如果我们希望传递给bind一个对象而又不拷贝它,就必须使用ref函数
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));

函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生辰搞一个保存const引用的类。与bind一样,函数ref和cref也定义在头文件functional中。

这篇关于C++深入学习笔记(18)—— 定制操作(lambda表达式)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!