C++ 中的 lambda 表达式

lambda 表达式

​ 现在有问题:求大于等于一个给定长度的单词有多少,并打印大于等于给定长度的单词。假设此函数为 biggies,其函数框架如下:

void bigges(vector<string> &words,vector<string>::size_type sz) {
    elimDups(words);              // 此函数将单词按字典序排序,并去重复 (传入的是引用,返回 void)
    sort(words.begin(), words.end(), isShorter);
    // 获取一个迭代器,指向第一个满足 size() >= sz 的元素
    // 计算满足 size >= sz 的元素数目
    // 打印大于等于给定长度的单词
}

​ 那么现在的新问题便是,如何获取一个迭代器,指向第一个满足 size() >= sz 的元素。假设知道其位置,就可以直接计算出有多少元素满足长度大于等于给定值。

​ 我们可以使用标准库 find_if 来查找第一个具有特定大小的元素。类似 find,前两个参数表示一个输入范围,第三个参数是一个谓词。find_if 会对输入序列中的每个元素调用给定谓词。它返回第一个使谓词返回非0的元素,如果不存在则返回尾迭代器。

​ 编写一个函数,接受一个 string 和一个长度,并返回一个 bool 表示该 string 长度是否大于给定长度,这很容易。但是 find_if 接受一个一元谓词,意味着我们传递给 find_if 的任何函数都必须严格接受一个参数。没有任何办法能传递给它第二个参数来表示长度。为了解决此问题,需要另一些语言特性。

介绍 lambda

​ 我们可以向一个算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它是可调用的。即,如果 e 是一个可调用的表达式,则我们可以编写代码 e(args),其中 args 是一个逗号分隔的一个或多个参数列表。

​ 可调用对象有四种:函数,函数指针,重载了函数调用运算符的类和 lambda 表达式

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

[capture list] (parameter list) -> return type {function body}

其中,捕获列表 (capture list) 是一个 lambda 所在函数中定义的局部变量的列表 (通常为空),剩下的分别表示 参数列表、返回类型与函数体。与普通函数不同,lambda 必须使用尾置返回。

​ 我们可以忽略参数列表和返回类型,但必须有捕获列表和函数体

auto f = [] { return 42; };

上面代码定义了一个可调用对象 f,它不接受任何参数,返回 42。lambda 的调用方式和普通函数相同:

cout << f() << endl;			// 打印 42

在 lambda 中忽略括号和参数列表等价于指定一个空参数列表。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个 return 语句,则返回类型从返回的表达式类型推断而来。否则返回 void。

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

向 lambda 传递参数

​ 与普通函数调用类似,调用一个 lambda 时给定的实参被用来初始化 lambda 的形参。通常,实参和实参的类型必须匹配。但是 lambda 不能有默认参数。因此,一个 lambda 调用的实参数目永远与形参数目相等。一旦形参初始化完成,就可以执行函数体了。

​ 我们可以编写与 isShorter 函数完成相同功能的 lambda:

[] (const string &s1,const string &s2)
	{ return s1.size() < s2.size(); }

空捕获列表表示此 lambda 不使用它所在函数中的任何局部变量。其他的部分和 isShorter 函数类似。这里可以不用显示指出返回类型,lambda 会自动推断,当然也可以显示指出:

[](const string &s1,const string &s2) -> bool
	{ return s1.size() < s2.size(); }

我们便可以这样调用 sort 函数:

sort(words.begin(), words.end(),
    	[](const string &s1,const string &s2)
     		{ return s1.size() < s2.size(); });
使用捕获列表

​ 到现在,我们便可以解决之前 由于 find_if 的第三个参数是一元谓词而只能接受一个参数,不能接受长度的问题了。

​ 虽然 lambda 可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。lambda 通过将局部变量包含在其捕获列表中来指出将会使用这些变量。

​ 这里我们就可以这样来完成 find_if 的第三个参数:

[sz] (const string &a)
		{ return a.size() >= sz; }

如果 sz 没有出现在捕获列表,就不能在 lambda 函数内部使用 sz。

注意:捕获列表只用于局部非 static 变量,lambda 可以直接使用局部 static 和在它所在函数之外声明的名字

调用 find_if

此时我们可以写出完整的 find_if:

auto wc = find_if(words.begin(), words.end(),
                 					[sz](const string &s)
                  							{ return s.size() >= sz; });

那么可以很容易的得出 words 中长度大于等于 sz 的数目:

auto count = words.end() - wc;
for_each 函数

​ 最后一个问题便是打印满足条件的元素。我们可以使用 for_each,该函数接受一个可调用对象,并对输入序列中每一个元素调用此对象:

for_each(wc, words.end(),
        			[](const string &s) { cout << s << " "; });
完整的 biggies

​ 到这一步,我们可以完整的写出 biggies 函数:

void biggies(vector<string> &words,vector<string>::size_type sz) {
    elimDups(words);
    sort(words.begin(),words.end(),
            [](const string &s1,const string &s2)
            { return s1.size() < s2.size(); });
    auto wc = find_if(words.begin(),words.end(),
            [sz](const string &s) { return s.size() >= sz; });
    
    auto count = words.end() - wc;
    cout << count << endl;
    for_each(words.begin(),words.end(),
            [](const string &s) { cout << s << " "; });
    cout << endl;
}
lambda 捕获和返回
值捕获

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

void fcn1() {
    size_t v1 = 42;
    auto f = [v1] { return v1; };
    v1 = 0;
    auto j = f();		// j 为 42,f 保存了我们创建它时 v1 的拷贝
}

由于被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝,所以 j 是 42。

引用捕获

​ 定义 lambda 时可以采用引用方式捕获变量。如:

void fcn2() {
    size_t v1 = 42;
    auto f = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();									// 此时的 j 为 0,而不是 42,因为 f2 保存的 v1 的引用而不是拷贝
}

​ 引用捕获与返回引用有相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 lambda 执行时是存在的。lambda 捕获的都是局部变量,这些变量在函数结束后就不存在了。如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

​ 引用捕获有时是必要的,如流对象,不能被拷贝,所以只能引用。如,我们希望 biggies 接受一个 ostream 的引用,用来输出数据,并接受一个字符作为分隔符:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') {
    // 与之前的代码一致
    // 打印:
    for_each(words.begin(),words.end(),
            [&os,c](const string &s) { os << s << c; });
}

建议:尽量保持 lambda 的变量捕获简单化

​ 确保 lambda 每次执行时的信息都有预期的意义,是程序员的责任。当捕获指针,迭代器或者采用引用捕获方式时,一定确保在 lambda 执行时,绑定到迭代器,指针或引用的对象仍然存在。

​ 一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。

隐式捕获

​ 除了显示的指出所用变量外,还可以让编译器根据 lambda 中的代码推断我们要使用哪些变量。为此,应该在捕获列表中用 & 告诉编译器采用引用捕获,= 采用值捕获。如,我们可以这样重新 find_if

wc = find_if(words.begin(), words.end(),
            				[=] (const string &s) { return s.size() >= sz; });

当然,可以混用隐式捕获和显式捕获:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') {
    // 与之前的代码一致
    // 打印:
    for_each(words.begin(),words.end(),
            [&os,=](const string &s) { os << s << c; });
    // 或者:
    for_each(words.begin(),words.end(),
            [&, c](const string &s) { os << s << c; });
}

但是当我们混合使用时,捕获列表中的第一个元素必须是 & 或 =。用此来指定默认捕获方式。

可变lambda

​ 默认情况下,对于一个值被拷贝的变量,lambda 不会改变其值。如果我们希望改变一个被捕获的变量的值,就必须加上 mutable 关键字。

void fcn3() {
    size_t v1 = 42;
    auto f = [v1] () mutable { return ++ v1; };			// 并不会改变函数中 v1 的值
    v1 = 0;
    auto j = f();																	// j 为 43
    // cout << v1 << endl;											 	// 输出 0
}

一个引用捕获的变量是否可以修改依赖于此引用指向的是否是 const 类型

void fcn4() {
    size_t v1 = 42;
    auto f2 = [&v1] { return ++ v1; };
    v1 = 0;
    auto j = f2();							// j 为 1
    // cout << v1 << endl;											// 输出 1
}
指定 lambda 返回类型

​ 前面说过,如果一个 lambda 包含 return 之外的任何语句,没有指定返回类型,编译器假定此 lambda 返回 void。所以,很多时候必须显式指定。

​ 如:返回一个数的绝对值,我们可以这样:

[] (int i) { return i < 0 ? -i : i; }

但是如果用 if-else,就必须这样:

[](int i) -> int { if(i < 0) return -i; else return i; }

定义返回类型时,必须使用尾置返回类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值