前言
上几期我们介绍了类的新功能,右值引用、完美转发语法特性,本期继续介绍C++11的新语法特性,即lambda表达式!
目录
前言
lambda表达式
lambda的引入
什么是lambda 表达式
lambda表达式的语法
捕捉列表说明
lambda的底层原理
什么是uuid?
lambda表达式
lambda 表达式 源于数学中的 λ
演算,λ
演算是一种 基于函数的形式化系统,它由数学家 阿隆佐邱奇 提出,用于研究抽象计算和函数定义。对于编程领域来说,可以使用 lambda 表达式 快速构建函数对象,作为函数中的参数。下面我们就来介绍一下C++中的 lambda !
lambda的引入
在C++98中,如果要想对一个数据的集合进行排序,如果是内置类型则可以直接使用std::sort函数:
int nums[] = { 5,2,6,1,9,34,21,11 };
cout << "排序前:";
for (const auto& e : nums) cout << e << " ";
cout << endl;
sort(nums, nums + sizeof(nums) / sizeof(nums[0]));// 默认是升序
cout << "升序排序:";
for (const auto& e : nums) cout << e << " ";
cout << endl;
sort(nums, nums + sizeof(nums) / sizeof(nums[0]), greater<int>());// 使用greater仿函数,控制成降序
cout << "降序排序:";
for (const auto& e : nums) cout << e << " ";
cout << endl;
注意:这里使用sort时,需要引入 <algorithm> 这个算法头文件!
这里的greater<T>()不在介绍了,我们在前面优先级队列以及介绍sort的时候就介绍了!
OK,上述的内置类型排序没有问题;但是现实中只对内置类型排序的场景很少,一般都是对自定义类型的某个属性排序,而自定义排序时用户需要自己指定比较规则:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> goods = { {"苹果",9.9, 7}, {"香蕉", 10.3, 8}, {"西瓜", 1.2,9} };
return 0;
}
例如现在需要对,goods的元素以 价格最高(降序)和价格最低(升序)、评价最高(降序)和评价最低(升序)进行对商品排序;此时就需要用户自己写出四个仿函数,来控制:
1、价格升序
struct CmpPriceLess
{
bool operator()(const Goods& a, const Goods& b)
{
return a._price < b._price;
}
};
2、价格降序
struct CmpPriceGreater
{
bool operator()(const Goods& a, const Goods& b)
{
return a._price > b._price;
}
};
3、评价升序
struct CmpEvaluateLess
{
bool operator()(const Goods& a, const Goods& b)
{
return a._evaluate < b._evaluate;
}
};
4、评价降序
struct CmpEvaluateGreater
{
bool operator()(const Goods& a, const Goods& b)
{
return a._evaluate > b._evaluate;
}
};
OK,这样实现了对需求的排序!但是这样写,属实有点麻烦!每次都要实现一个类,如果比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这给程序员带来了极大的不便。因此,C++11引入了lambda表达式!
什么是lambda 表达式
OK,我就直接先说结论了!
lambda表达式的本质就是一个匿名函数对象!
这里你可能不理解,我都没见过lambda表达式是啥样,你一上来就给我纯理论!确实这里,有些残暴了!不过没关系,我们下面先来见一见,然后在理解这句话:
vector<Goods> goods = { {"苹果",9.9, 7}, {"香蕉", 10.3, 8}, {"西瓜", 1.2,9} };
sort(goods.begin(), goods.end(), [](const Goods& a, const Goods& b)
{
return a._price < a._price;// 价格升序
});
sort(goods.begin(), goods.end(), [](const Goods& a, const Goods& b)
{
return a._price > a._price;// 价格降序
});
sort(goods.begin(), goods.end(), [](const Goods& a, const Goods& b)
{
return a._evaluate < a._evaluate;// 评价升序
});
sort(goods.begin(), goods.end(), [](const Goods& a, const Goods& b)
{
return a._evaluate > a._evaluate;// 评价降序
});
上面的四个仿函数对直接没了,直接变成了后面的这些!为什么说lambda的本质就是一个匿名的函数对象呢?以前,这个位置不就是一个仿函数的对象吗!现在只不过是,把这个对象换成了直接直接实现罢了,且没有名字!
lambda表达式的语法
OK,上面见了一下,但是你可能有些懵,没关系我们下面就来介绍lambda表达式的语法!
lambda表达式的书写格式:
[capture-list] (parameters) mutable -> return-type {statement};
lambda表达式的各部分说明:
[capture-list] :捕捉列表,该列表总是出现在lambda表达式的开始位置(不可省略,但是可以为空),编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量,供lambda函数使用!
(parameters) :参数列表。与普通函数的参数列表一样,如果不需要传递参数,则可以连同()一块省略!
mutable : 取消参数的常性。一般情况下,lambda表达式总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即参数为空)!
-> return-type :返回值类型。用于追踪返回类型形式声明函数的返回值,没有返回值这部分则可以省略。返回值类型明确的情况下,也可以省略,由编译器自动推导!
{statement} :函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕捉到的变量!
注意:在lambda函数定义中,参数列表和返回值类型都是可选的部分(可省略),而不追列表和函数体是必须的,但是可以为空!因此在C++11中,最简单的lambda函数为:[]{}; 该lambda函数不做任何的事情!
OK,有了对语法的介绍,我们就可以理解上面写的排序了:
sort(goods.begin(), goods.end(), [](const Goods& a, const Goods& b)
{
return a._evaluate > a._evaluate;// 评价降序
});
这里,因为只是比较,不对参数进行修改,所以没加 mutable , 而函数体内的返回值显然是 bool 类型,所以直接省略掉返回值类型!当然你也可以加上:
当然吗,我们也可以自己写一写,lambda函数:
[] {};// 最简单的 lambda函数,无实际的意义
[](const int& a, const int& b) ->int {return a + b; };
OK,第一个我么是介绍过的;这里主要看第二个:这是一个简单的add函数,实现的是两个整数的加法(返回值可省略)!现在的问题是如何调用这个匿名函数,匿名函数对象的生命周期是在当前行,所以,第一种用法就是直接调用:
第二种就是,使用auto 推导出匿名函数的类型,然后赋值给一个变量:
第二种,这里就是我们以前介绍的移动构造/拷贝构造;因为过了当前行匿名函数就要销毁了,他就是一个将亡值,此时如果这个类实现了移动构造就会把这个对象的资源转移给add,如果没有实现移动构造就会调用拷贝构造!这里这样说你可能不太理解,这得介绍了lambda的原理才可以理解!后面会介绍 ,也会在提这个点的!
捕捉列表说明
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式是产值还是传引用。
• [var]:表示值传递的方式捕捉变量var(默认是const修饰的)
• [=]:表示值传递的方式捕捉所有父作用域中的变量,包含this
• [&var]:表示引用传递捕捉变量var
• [&]:表示引用传递的方式捕捉所有父作用域中的变量,包含this
• [this]:表示值传递的方式捕捉当前的this指针
注意:
1、父作用域指的是包含lambda函数的语块
2、语法上捕捉列表可由多个捕捉项组成,并以逗号分割
例如:[&, a,this]表示以引用的方式捕捉除了a,this以外的所有变量,a/this用传值捕捉!在如:[=,&a,&b]表示:以传值的方式捕捉除了a和b以外的所有变量,用引用的方式捕捉a和b;
3、捕捉列表不允许变量重复传递,否则就会导致编译错误。
例如:[=,a],此时,a已经被捕捉,后面有传递就会导致重复捕捉!
4、在块作用域以外的lambda函数捕捉列表必须为空!
这句话的意思就是说,如果一个全局的lambda的函数,它的捕捉列表必须为空!因为他没有父作用域!
5、在块作用域以外的lambda函数仅能捕捉父作用域中的局部变量,捕捉任何非此作用域中的局部变量或者全局变量都会导致编译报错
6、lambda表达式直接不能相互赋值,即使看起来类型相同也不行!
第6点这个也是要结合底层原理理解的!后面再提,这里先看看:
看到这个报错,我们大概猜测lambda的底层会不会就是仿函数呢?其实是的!
OK,我们到此lambda的语法基本介绍完了,我们来写几个栗子,用一下:
1、演示mutable的作用
我们知道[var]默认捕捉的值是被const修饰的,如果我们想让他修改,该如何做呢?就用mutable,告诉编译器该捕捉可以修改:我们以经典的两数交换来演示
此时,是不允许a和b修改的,要想修改加上mutable即可:
此时,由于是传值捕捉,所以只是改变了形参,外面的实参依旧是没变:
2、演示[=]捕捉
int a = 3, b = 5;
auto f = [=] {return a + b; };
cout << f() << endl;
这里没有对捕捉的值修改,所以不加mutable,如果修改就继续得加上!
3、验证[&var]捕捉
int a = 3, b = 5;
auto swap1 = [&a, &b]() mutable
{
int tmp = a;
a = b;
b = tmp;
};
此时这里加不加mutable都是一样的!因为此时就是外面的别名!
4、验证[&]捕捉
int a = 3, b = 5;
auto swap1 = [&]()
{
int tmp = a;
a = b;
b = tmp;
};
虽然最后两种写法也可以实现效果,但是标准的写法,swap的标准写法是:
auto swap = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
lambda的底层原理
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(100, 1);
return 0;
}
我们可以跳到反汇编看看,底层实现:
我们可以看到,lambda的底层就是去创建了一个类,这个类是lambda_uuid,然后又去调用了(), 也就是说,lambda表达式是编译器帮你创建了一个lambda_uuid的类,然后重载实现了(),最后去调用了()
什么是uuid?
UUID是Universally Unique Identifier(通用唯一识别码)的缩写,它是一个128位(16字节)长的数字,用于确保在时间和空间上的唯一性。
它的优点是,生成的标识符几乎就是唯一的!所以我们每一个lambda表达式对应一个uuid的类,uuid的重复率极低,所以即使你的两个lambda一样,但是他们的uuid不一样!
我们可以写一个lambda看看:
auto f1 = [] {cout << "hello world"; };
auto f2 = [] {cout << "hello world"; };
cout << typeid(f1).name() << endl;
cout << typeid(f2).name() << endl;
这也解释了我们前面的,为什么两个两相同的lambda不能赋值!原因是,他们底层的类一样!也就是不也是同一类型的对象!
为什么说是将匿名的lambda函数对象赋值给一个变量是,走的移动构造/拷贝构造?原因是,编译器生成的这个类或生成一个默认的移动构造,而匿名函数的销毁时把资源转移给了那个引用的对象!
OK,好兄弟,本期分享就到这里,我是cp我们下期再见!