Lambda表达式
- 1 Lambda使用的一个例子
- 2 Lambda 表达式的语法
- 3 初次体验Lambda表达式
- 4 Lambda函数底层实现原理
1 Lambda使用的一个例子
在C++98中,如果我们想要对一个自定义类型进行排序,就需要用户自定义去书写比较的规则。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Goods
{
public:
Goods(string name, double price, int evaluate)
:_name(name), _price(price), _evaluate(evaluate)
{}
string _name;//名字
double _price;//价格
int _evaluate;//评价
};
struct ComparePriceLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 },
{ "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceGreater());//按价格降序排列
cout << "降序排列:" << endl;
for (int i = 0; i < v.size(); i++)//打印结果
{
cout << v[i]._name << v[i]._price << " " << v[i]._evaluate << endl;
}
cout << endl;
cout << "升序排列:" << endl;
sort(v.begin(), v.end(), ComparePriceLess());//按价格升序排列
for (int i = 0; i < v.size(); i++)//打印结果
{
cout << v[i]._name << v[i]._price << " " << v[i]._evaluate << endl;
}
return 0;
}
随着C++的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法就要去重新写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,C++11语法中新增了Lambda表达式。
//按价格降序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; })
//按价格升序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; })
上述的代码就是Lambda表达式来解决,可以看出Lambda表达式实际上是一个匿名函数。
2 Lambda 表达式的语法
[capture-list] (parameters) opt -> return-type {statement}
[cpature] 为捕获列表(不捕获时,[]不能省略)
①[]、[&]和[=]分别表示不捕获、按引用捕获、按值捕获所有父作用域中内的局部变量。(父作用域指包含lambda表达式的语句块,如main函数)。
◆lambda函数只能捕获父作用域中的局部变量,而捕获非父作用域或static变量都会报错(不是C++11的标准,其行为可能因编译器而不同)。(注意全局变量或static变量不能被捕获。即不能被写入捕获列表中,但可在lambda的函数体内直接访问)
◆默认下无法修改按值捕获的外部变量(因为lambda表达式的operator()默认是const函数,但捕获this指针后可修改成员变量的值,因为是通过this指针来修改的)。(涉及到lambda的底层原理,后面会讲)
◆在类中如果使用&或=捕获,会同时默认捕获this指针。
②[=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。注意,捕获列表不允许变量重复传递,如[=,var],var被按值捕获了两次,这是不允许的。
③[bar]按值捕获bar变量(注意只捕获bar,其他变量不被捕获)
④[this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样访问权限,可以使用当前类的成员函数和成员变量(注意也可修改non-const成员变量的值)。
(parameters): Lambda表达式的参数列表(不需要参数传递时,()可以省略)
①如果省略,则类似于无参函数func()。
②参数列表中不能有默认参数。所有的参数必须有参数名。
③不支持可变参数。
opt选项(可省略)
①mutable修饰符:默认下,lambda表达式的operator()是const函数,mutable可以取消其常量性,让body内的代码可以修改被捕获的变量,并可以访问被捕获对象的non-const函数。在使用该修饰符时,参数列表不可省略(即使参数为空)。
②exception:说明lambda表达式是否抛出异常(noexcept),以及抛出何种异常。如抛出整数类型的异常,可以使用throw(int)。
③attribute用来声明属性
-return-type :返回值类型(没有返回值时可省略)
①如果被省略了,则由return语句的返回类型确定
②如果没有return语句,则类似于void func(…)函数。
{statement}:函数体。
在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
3 初次体验Lambda表达式
在Lambda函数定义中,参数列表和返回值类型都是可选部分,而捕获列表和函数体部分可以为空,所有最简单的Lambda函数为:[]{};但是该Lambda不能做任何事。
省略参数列表和返回值类型
//省略参数列表和返回值类型,返回值类型由编译器自动推导为int
int a = 1, b = 2;
[=] {return a + 3; };//[=] 表示按值捕获 函数体里的a是一份拷贝
省略返回值类型
//省略返回值类型,无返回值类型
int a = 1, b = 2;
auto fun = [&](int c) {b = a + c; };//[&]表示按引用捕获a和b
fun(10);//调用Lambda函数
cout << "a=" << a << endl;//a=1
cout << "b=" << b << endl;//b=11
各部分都很完善的Lambda函数
//各部分都很完善的Lambda函数
int a = 1, b = 2;
//[=,&b]表示除了b按引用捕获,其他都按值捕获
auto fun = [=, &b](int c) {b += a + c; };
fun(10);
cout << "b=" << b << endl;//b=13
mutable
int x = 10;
//mutable表示取消按值捕获的x的常量性,不加mutable无法修改x的值
//但是一般都是捕获x的引用,即[&x]
auto add_x = [x](int a) mutable {return x += a; };
cout << "x=" << add_x(10) << endl;//x=20
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
在类中使用Lambda表达式
#include <iostream>
using namespace std;
int g = 0;
class Test
{
private:
int i = 0;
public:
void func(int x, int y)
{
//auto x1 = []{return i;}; //error,没有捕获任何变量。当然也无法捕获父作用域(func)以外的变量,因此i是不能访问到的!
//auto x1 = [&i]{return i;}; //error,无法捕获父作用域(func)以外的变量
auto x1 = [=]{return i++;}; //ok,因为“=”或“&”默认会捕获this指针,也就可以访问到成员变量。根据按值捕获的特点,此时
//在lambda的函数体内不能修改this指针本身。但这不影响我们通过this指针修改成员变量i的值!
auto x2 = []{return g++;}; //ok,g不在lambda的父作用域,不能被捕获。
//如auto x2 = [&g]{return g++;}。但由于g是全局变量
//所以在lambda的body内仍是可见的!
auto x3 = [=]{return i++, i + x + y;};//ok,按值捕获所有外部变量,由于&或=捕获时会默认地同时传入this,所以可改变i的值
auto x4 = [&]{return i + x + y;}; //ok,按引用捕获所有变量:x, y以及this指针
//但注意,i没有被捕获,也不可捕获到。它是通过this指针来访问到的。
auto x5 = [this]{return i++;}; //ok,捕获了this指针,也就可以修改成员的值
//auto x6 = [this, x]{return i + x + y;}; //error,没捕获y
}
};
4 Lambda函数底层实现原理
仿函数是编译器实现lambda表达式的一种方式。在现阶段,通常编译器会把lambda表达式转化成一个仿函数对象。因此在C++11中,lambda可以视为仿函数的一种等价形式。
我们以这段代码,转到汇编层面查看它的原理
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.49;
Rate r1(rate);
r1(10000, 2);
// Lambda
auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可 以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
从底层延申出来的一些问题
1.由于每定义一个lambda表达式,编译器会自动生成一个类,所以Lambda表达式之间是不能互相赋值的,即使看起来类型一样。
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
return 0;
}
2.一个Lambda表达式大小由捕获的变量决定,如果无捕获变量,则大小为1字节。
int main()
{
int a = 1;
auto f1 = [&a] {};
auto f2 = [] {};
cout << "f1的大小:" << sizeof(f1) << endl;//4
cout << "f2的大小:" << sizeof(f2) << endl;//1
return 0;
}
原因如该图所示:
底层会由捕获列表创建出相应的变量。
至于为什么无捕获时大小为1,很简单,C++标准规定的。
想要深究的话可以看看这篇文章
为什么lambda无捕获时大小为1字节
lambda表达式在C++11中被称为“闭包类型(Closure Type)”,可以认为它是一个带有operator()的类(即仿函数),它的捕获列表捕获的任何外部变量最终均会变为闭合类型的成员函数。没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。