Lambda表达式用法超详细整理!!!

2021-11-15 10:18:25 浏览数 (1)

文章较长,坚持看完,相信对你一定会有所收获!

Lambda我们可以将其理解为一个未命名的内联函数。

与任何函数类似,一个lambda具有一个返回类型,一个参数列表和一个函数体。

但与函数不同,lambda可能定义在函数内部。

一个lambda表达式具有如下形式:

capture list ->return type {function body}

capture list: 捕获列表,是一个lambda所在函数中定义的局部变量列表(通常为空)

parameter list:参数列表

return type:返回类型

function body:函数体

但是与普通函数不同,lambda必须使用尾置返回来指定返回类型

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

代码语言:javascript复制
auto f=[]{return 42;};//分号不能丢

此例中,我们定义了一个可调用对象f,它不接受参数,返回42.

lambda的调用方式与普通函数调用方式相同,都是使用调用运算符:

代码语言:javascript复制
cout<<f()<<endl;//打印42

在lambda中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用f时,参数列表是空的。如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。

如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来,否则,返会类型为void.

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

向lambda传递参数

与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数

因此,一个lambda调用的实参数目永远与形参数目相等。

下面举一个带参数的lambda的例子:

代码语言:javascript复制
[](const string &a,const string& b)
{return a.size()<b.size();};//这里分号不能丢

空捕获列表表面此lambda不使用它所在函数中的任何局部变量。

使用捕获列表

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

下面举一个例子:

代码语言:javascript复制
#include<iostream>
using namespace std;
void test()
{
    string sz("abc");
    string words("abds");
    auto ret=[sz](const string& a)
    {
        return a.size() >= sz.size();
    };
    cout << ret("efgdasd") << endl;
}
int main()
{
    test();
    system("pause");
    return 0;
}

lambda以一对[]开始,我们可以在其中提供一个以逗号分隔的名字列表(当前函数中定义的局部变量),这些名字都是它所在函数中定义的。

上面例子中由于lambda只捕获了test函数中局部变量sz,因此可以在lambda的函数体中使用sz.lambda不捕获words,因此不能在lambda的函数体中访问此变量。如果我们给lambda提供一个空捕获列表,则代码会编译出错:

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

lambda表达式在泛型算法的应用

与find_if结合使用

举例:调用find_if算法在字符串s中查找第一个长度大于等于字符串sz的元素

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//用lambda作为参数                                                              
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	vector<string>::iterator pos = find_if(words.begin(), words.end(), [sz](const string& a) {return a.size() > sz; });
	cout << *pos << endl;
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

这里对find_if的调用返回一个迭代器,指向第一个长度不小于sz的元素。如果这样的元素不存在,则返回words.end()的一个拷贝

我们可以使用find_if返回的迭代器来计算从它开始到words的末尾一共有多少个元素。

代码语言:javascript复制
//用lambda作为参数                                                              
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	vector<string>::iterator pos = find_if(words.begin(), words.end(), [sz](const string& a) {return a.size() > sz; });
	//这里两个迭代器做减法,类似指针做减法,得到两个迭代器之间的距离
	//与指针不同,我们无法直接打印迭代器,例如cout<<pos<<endl;
	auto count = words.end() - pos;
	cout << count<< endl;
}

与for_each结合使用

举例:打印words中长度大于等于sz的元素。

代码语言:javascript复制
//用lambda作为参数                                                              
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	vector<string>::iterator pos = find_if(words.begin(), words.end(), [sz](const string& a) {return a.size() > sz; });
	for_each(pos, words.end(), [](const string& a) {cout << a << " "; });
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

此lambda中的捕获列表为空,但其函数体中还是使用了两个名字:s和cout,前者是它自己的参数。

捕获列表为空,是因为我们只对lambda所在的函数中定义的(非static)变量使用了捕获列表。一个lambda可以直接使用定义在当前函数之外的名字。在本例中,cout不是定义在bigger中的局部名字,而是定义在头文件iostream中。因此,只要在bigger出现的作用域中包含头文件iostream,我们的lambda就可以使用cout.

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

完整的biggerd代码:

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//删除重复元素
void elimDups(vector<string>& words)
{
	//再删除重复元素之前,需要先进行排序
	sort(words.begin(), words.end());
	//把重复元素移到尾部
	auto new_end = unique(words.begin(), words.end());
	//将尾部重复元素删除
	words.erase(new_end, words.end());
}
//用lambda作为参数                                                              
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	//将words按字典序重排,并消除重复单词
	elimDups(words);
	//按长度重新排序,长度相同的单词维持字典序
	stable_sort(words.begin(), words.end(), [](const string& a1, const string& a2) {return a1.size() > a2.size(); });
	//获取一个迭代器,指向第一个满足size>=sz的元素
	vector<string>::iterator pos = find_if(words.begin(), words.end(), [sz](const string& a) {return a.size() > sz; });
	//计算满足size>=sz的元素的数目
	auto count = count_if(words.begin(), words.end(), [sz](const string& a) {return a.size()>sz; });
	cout << count << endl;
	//打印长度大于等于给定值的单词,每个单词后面接一个空格
	for_each(pos, words.end(), [](const string& a) {cout << a << " "; });
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

类似参数传递,变量的捕获方式也可以是值或引用。

值捕获

与传值参数类似,采用值捕获的前提是变量可以拷贝。

与参数不同,被捕获的变量的值实在lambda创建时拷贝,而不是调用时拷贝

举例:

代码语言:javascript复制
#include <iostream>
using namespace std;
void test()
{
	size_t v1 = 42;//局部变量
	//将v1拷贝到名为f的可调用对象
	auto f = [v1] {return v1; };
	v1 = 0;
	auto j = f();
	cout << j << endl;//j为42,f保存了我们创建它时的拷贝
}
int main() 
{            
	test();
	return 0;
}

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

引用捕获

举例:

代码语言:javascript复制
#include <iostream>
using namespace std;
void test()
{
	size_t v1 = 42;//局部变量
	//f对象包含v1的引用
	auto f = [&v1] {return v1; };
	v1 = 0;
	auto j = f();
	cout << j << endl;//j为0,f保存了v1的引用,而非拷贝
}
int main() 
{            
	test();
	return 0;
}

当我们在lambda函数体内使用此变量时,实际上使用的时引用所绑定的对象。

引用捕获和返回引用的注意事项:

如果我们采用引用的方式捕获了一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。

lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。

如果lambda可能在函数结束后执行,捕获的引用执行的局部变量已经消失。

引用捕获有时候是必要的,例如:

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;                                                         
void bigger(vector<string>& words ,ostream &os=cout,char ch=' ')
{
	for_each(words.begin(), words.end(), [&os, ch](const string& s) {os << s << ch; });
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec);
}

我们不能拷贝ostream对象,因此捕获os的唯一方法就是捕获其引用(或指向os的指针)。

注意:当以引用方式捕获一个变量的时候,必须保证lambda指向时变量是存在的

对lambda变量捕获部分重点总结:

捕获一个普通变量,如int,string或其他非指针类型,通常采用简单的值捕获方式。

如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须保证对象具有预期的值。

在lambda从创建到它执行这段时间内,可能有代码改变绑定对象的值。

也就是说,在该指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在lambda执行时,该对象的值已经完全不同了。

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

隐式捕获

通过在捕获列表中写一个&或=,指示编译器推断捕获列表。

&告诉编译器采用引用方式,=则表示采用值捕获方式

例如:

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;                                                             
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	//sz为隐式捕获,值捕获方式
	auto wc = find_if(words.begin(), words.end(), [=](const string& s) {return s.size() >= sz; });
	cout << *wc << endl;
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显示捕获:

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;                                                             
void bigger(vector<string>& words,vector<string>::size_type sz,ostream& os=cout,char c=' ')
{
      //os为隐式捕获,引用捕获方式
	 //c显示捕获,值捕获方式
	for_each(words.begin(), words.end(), [&, c](const string& s) {os << s << c; });
	cout << endl;
	//os显示捕获,引用捕获方式
	//c隐式捕获,值捕获方式
	for_each(words.begin(), words.end(), [=,&os](const string& s) {os << s << c; });
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

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

当混合使用隐式捕获和显示捕获时,显示捕获的变量必须使用与隐式捕获不同的方式。

即如果隐式不会是引用方式,则显示捕获命名变量必须采用值方式,因此不能在其名字前使用&.

类似的,如果隐式捕获采用的是值方式,则显示捕获命名的变量必须采用引用方式,即在名字前使用&。

总结lambda捕获列表:

1、空。没有使用任何函数对象参数。

2、=。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。

3、&。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。

4、this。函数体内可以使用Lambda所在类中的成员变量。

5、a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。

6、&a。将a按引用进行传递。

7、a, &b。将a按值进行传递,b按引用进行传递。

8、=,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。

9、&, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

可变lambda

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值,如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:

代码语言:javascript复制
#include <iostream>
using namespace std; 
void test()
{
	size_t val = 42;//局部变量
	//f可以改变它所捕获的变量的值
	auto f = [val]()mutable {return val  ; };
	val = 0;
	cout << f() << endl;
	cout << f() << endl;
	cout << f() << endl;
	cout << val << endl;
}
int main()
{
	test();
	return 0;
}

多次调用函数f,就可以多次对f保存的val进行累加改变。

但val本身值不变

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

代码语言:javascript复制
#include <iostream>
using namespace std; 
void test()
{
	size_t val = 42;//局部变量
	//val是一个非const变量的引用
	//可以通过f中的引用来改变它
	auto f = [&val]() {return val  ; };
	val = 0;
	cout << f() << endl;
	cout << f() << endl;
	cout << f() << endl;
	cout << val << endl;
}
int main()
{
	test();
	return 0;
}

指定lambda的返回类型

默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void.

与其他返回void的函数类似,被推断为返回void的lambda不能有返回值。

举例:

代码语言:javascript复制
#include <iostream>
#include<algorithm>
using namespace std; 
void test()
{
	int arr[5] = { -1,-2,-3,-4,-5 };
	transform(arr, arr   5,arr,[](int i) {return i < 0 ? -i : i; });
	for (int i = 0; i < 5; i  )
	{
		cout << arr[i];
	}
	cout << endl;
}
int main()
{
	test();
	return 0;
}

本例中lambda体中只有单一的return语句。我们无需指定返回类型,因为可以根据条件运算符的类型推断出来。

但是如果我们将程序改写成看起来是等价的if语句,就会产生编译错误:

虽然这里没有发生错误,是因为版本问题,有些低版本编译器会出现问题,原因在于:

编译器推断这个版本的lambda返回类型为void,但它返回一个int值。

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

代码语言:javascript复制
#include <iostream>
#include<algorithm>
using namespace std; 
void test()
{
	int arr[5] = { -1,-2,-3,-4,-5 };
	transform(arr, arr   5, arr, [](int i)->int{if (i < 0) return -i;else return i;});
	for (int i = 0; i < 5; i  )
	{
		cout << arr[i];
	}
	cout << endl;
}
int main()
{
	test();
	return 0;
}

lambda是函数对象----->函数对象—>重载()运算符—>operator()

当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。

在lambda表达式产生的类中含有一个重载的函数调用运算符。

举例:

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;                                                             
void bigger(vector<string>& words,vector<string>::size_type sz)
{
       //根据单词的长度对其进行排序
	stable_sort(words.begin(), words.end(), [](const string& a1, const string& a2) {return a1.size()<a2.size(); });
	for_each(words.begin(), words.end(), [](const string& a) {cout << a << " "; });
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

该lambda的行为类似于下面这个类的一个未命名对象

函数对象的概念

仿函数做泛型算法的参数又细分为一元谓词和二元谓词,不了解的建议去看看:

谓词的概念

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std; 
class ShortString
{
public:
	bool operator()(const string& a1, const string& a2)
	{
		return a1.size() < a2.size();
	}
};
void bigger(vector<string>& words,vector<string>::size_type sz)
{
       //根据单词的长度对其进行排序
	stable_sort(words.begin(), words.end(), ShortString());
	for_each(words.begin(), words.end(), [](const string& a) {cout << a << " "; });
}
int main() 
{                                                     
	vector<string> svec{ "the", "quick", "red", "fox", "jumps","over", "the", "slow", "red", "turtle" };
	bigger(svec, 4);
}

默认情况下lambda不能改变他捕获的变量。是因为由lambda产生的类中的函数调用的运算符是一个const成员函数。如果lambda被声明为可变的,则调用运算符就不是const的了。

表示lambda及相应捕获行为的类

当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引的对象确实存在。因此,编译器可以直接使用该引用而无需在lambda产生的类中将其存储为数据成员。

相反,通过值捕获的变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获捕获的变量的值来初始化数据成员。

举个例子:

代码语言:javascript复制
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	auto wc = find_if(words.begin(), words.end(), [sz](const string& a) {return a.size() >= sz; });
}

该lambda表达式的产生的类的将形如:

代码语言:javascript复制
class SizeComp
{
private:
	size_t sz;//该数据成员对应通过值捕获的变量
public:
	SizeComp(size_t n):sz(n){}//该形参对应的捕获变量
	//该调用运算符的返回类型,形参和函数体都与lambda一致
	bool operator()(const string& s)const
	{
		return s.size() >= sz;
	}
};

上面这个类含有一个数据成员以及一个用于初始化该成员的构造函数。

这个合成的类不含有默认构造函数,因此想使用这个类必须提供一个实参:

代码语言:javascript复制
void bigger(vector<string>& words,vector<string>::size_type sz)
{
	auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
}

lambda表达式产生的类不含默认构造函数,赋值运算符及默认析构函数;

它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。

完结撒花!!!

0 人点赞