0
点赞
收藏
分享

微信扫一扫

C++ lambda 表达式深剖


目录

  • ​​传统艺能😎​​
  • ​​概念🤔​​
  • ​​语法🤔​​
  • ​​捕获方式🤔​​
  • ​​相互赋值😎​​
  • ​​mutable🤔​​
  • ​​底层原理🤔​​

传统艺能😎


C++ lambda 表达式深剖_c++11

概念🤔

自 C++11 开始,C++ 有三种方式可以创建/传递一个可调用的对象:

函数指针
仿函数
Lambda 表达式

lambda 表达式本质上就是一个匿名函数,它是一个强大的功能,他的使用可以简化代码,而且可以提高代码可读性

这里举一个实例,以一个物品为例:

struct Items
{
string _name; //名字
double _price; //价格
int _num; //数量
};

如果要对若干对象分别按照价格和数量进行升序、降序排序。

首先想到可以使用 sort 函数,但由于这里待排序的元素为自定义类型因此需要用户自行定义排序时的比较规则,要控制 sort 比较方式有常见的两种方法,一是对商品类的的 () 运算符进行重载,二是通过仿函数来指定比较方式。

重载当前类的 () 运算符是不可行的,因为这里要求分别按照价格和数量进行升序、降序排序,每次排序都去修改一下比较方式是很低效且笨重的做法

比如我用仿函数的方式进行比较:

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;
}
};
struct CompareNumLess//数量降序
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._num < g2._num;
}
};
struct CompareNumGreater//数量升序
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._num > g2._num;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
sort(v.begin(), v.end(), ComparePriceLess()); //价格升序
sort(v.begin(), v.end(), ComparePriceGreater()); //价格降序
sort(v.begin(), v.end(), CompareNumLess()); //数量升序
sort(v.begin(), v.end(), CompareNumGreater()); //数量降序
return 0;
}

如你所见,仿函数也顺利解决了以上问题,但是如果定义和使用位置隔的很远就不好观察,这就要求取名的时候通俗易懂,这种情况下就更加推荐使用 lambda 表达式。

比如我这里有一个做加法的仿函数:

class Plus {
public:
int operator()(int a, int b) {
return a + b;
}
};

Plus plus;
std::cout << plus(11, 22) << std::endl;

我们把这个加法仿函数改写成 lambda 表达式就是

auto Plus = [](int a, int b) { return a + b; };

因为 lambda 表达式是一个匿名函数,该函数无法直接调用,这里我们借助 auto 将其赋值给一个变量,此时这个变量就可以像普通函数一样使用

这前后一对比,显而易见两者的优劣就高下立判了

语法🤔

lambda 表达式定义

[ capture-list ] ( params ) mutable(optional) exception(optional) attribute(optional) -> ret(optional) { body }

当然在书写格式上并不是必须写成一行,如果函数体太长可以进行换行
说明一下各个参数:

capture-list:捕捉列表。上面的例子 auto Plus = [](int a, int b) { return a + b; }; 就没有捕获任何变量
params:和普通函数一样的参数。
mutable:只有这个 Lambda 表达式是 mutable 的才允许修改按值捕获的参数。
-> ret:返回值类型,可省略,编译器可以通过 return 语句自动推导
body:具体函数

没说明的就暂时不必理解,其中lambda 参数列表和返回值类型都是可有可无的,但捕捉列表和函数体是不可省略的,因此最简单的lambda函数如下:

int main()
{
[]{}; //最简单的lambda表达式
return 0;
}

捕获方式🤔

Lambda 表达式最基本的两种捕获方式是:按值捕获按引用捕获

我们对捕获列表的说明描述了上下文中哪些数据可以被 lambda 函数使用,以及使用的方式是传值还是传引用

[var]:值传递捕捉变量var
[=]:值传递捕获所有父作用域中的变量(成员函数包括this指针)
[&var]:引用传递捕捉变量var
[&]:引用传递捕捉所有父作用域中的变量(成员函数包括this指针)
[this]:值传递捕捉当前的this指针

实际当我们以 [&](引用传递全捕获) 或 [=] (值传递全捕获)的方式捕获变量时,编译器也不一定会把父作用域中所有的变量捕获进来,编译器可能只会对 lambda 表达式中用到的变量进行捕获,实际要看编译器的具体实现

父作用域就是包含 lambda 表达式的语句块,语法上捕捉列表可由多个逗号分割的捕捉项组成,比如KaTeX parse error: Expected '}', got '&' at position 18: …olor{red} {[=, &̲a, &b]}

  1. 捕捉列表不允许重复传递,否则会导致编译错误,比如重复传递了变量 a
  2. lambda表达式之间不能相互赋值,即使看起来类型相同
  3. 在块作用域以外的 lambda 表达式捕捉列表必须为空!即全局lambda函数的捕捉列表必须为空,且在块作用域中的 lambda 表达式仅能捕捉父作用域中的局部变量,除此以外的都会导致编译报错。

相互赋值😎

lambda表达式之间不能相互赋值,就算是两个一模一样的也不行,这不邪门儿了嘛,为啥呢?

因为 lambda 表达式底层处理方式和仿函数是一样的(本文后面的底层原理部分有细谈),在VS下 lambda 表达式会被处理为函数对象,该函数对象对应的类名叫做<lambda_uuid>

类名中的uuid叫做通用唯一识别码,简单来说就是通过算法生成的一串字符串,它具有随机性和不重复性,保证在当前程序中每次生成不同的 uuid,因为 lambda 表达式底层的类名包含 uuid,这就保证了每个 lambda 表达式底层类名都是唯一的!

我们可以通过的方式来获取lambda表达式的类型来验证上述结论:

int main()
{
int a = 10, b = 20;
auto Swap1 = [](int& x, int& y)->void
{
int tmp = x;
x = y;
y = tmp;
};
auto Swap2 = [](int& x, int& y)->void
{
int tmp = x;
x = y;
y = tmp;
};
cout << typeid(Swap1).name() << endl; //class <lambda_797a0f7342ee38a60521450c0863d41f>
cout << typeid(Swap2).name() << endl; //class <lambda_f7574cd5b805c37a13a7dc214d824b1f>
return 0;
}

如你所见,就算是一模一样的 lambda 表达式,它们的类型也是不同的

mutable🤔

在实际使用中,比如实现一个交换函数,我们用 lambda 表达式实现:

int main()
{
int a = 1, b = 2;
auto Swap = [a, b]()
{
int tem = a;
a = b;
b = tem;
};
Swap();
return 0;
}

这里一眼就是传值捕获,但是真的可以吗?答案是 No,他的编译不会通过,因为传值捕获到的变量默认是不可修改的(const):

//值捕获的类型是 const 类型
int i = 100;
auto func = [i]() {
i = 200; // 编译错误:assignment of read-only variable ‘i’
};

如果要取消其常量属性,就需要在 lambda 表达式中加上 mutable 像这样:

auto Swap = [a, b]()mutable
{
int tem = a;
a = b;
b = tem;
};

但由于是传值捕捉,lambda 表达式中对局部变量的修改不会影响本身的变量,与函数的传值传参是一个道理,因此这种方法无法完成交换功能。

底层原理🤔

实际编译器在底层对于 lambda 表达式的处理方式,完全就是按照函数的方式处理的,函数对象就是我们平常所说的仿函数,就是在类中对 () 运算符进行了重载的类对象

我们编写了一个 Add 类,然后对 () 运算符进行了重载,因此 Add 类实例化出的 add1 对象就是函数对象,add1 可以像函数一样使用。接着写了一个 lambda 表达式,并借助 auto 将其赋值给 add2 对象,这时 add1 和 add2 都可以像普通函数一样使用

class Add
{
public:
Add(int base)
:_base(base)
{}
int operator()(int num)
{
return _base + num;
}
private:
int _base;
};
int main()
{
int base = 1;

//函数对象
Add add1(base);
add1(1000);

//lambda表达式
auto add2 = [base](int num)->int
{
return base + num;
};
add2(1000);
return 0;
}

我们再通过反汇编对代码进行观察:

C++ lambda 表达式深剖_c++11_05


创建函数对象 add1 时,会调用 Add 类的构造函数,使用 add1 时,会调用 Add 类的 () 运算符重载函数。

C++ lambda 表达式深剖_lambda 表达式_06

然后 lambda 表达式这边也是和函数的过程非常类似:在借助 auto 将 lambda 表达式赋值给 add2 对象时,会调用 <lambda_uuid> 类的构造函数,在使用add2对象时,会调用<lambda_uuid>类的()运算符重载函数

当我们定义一个lambda表达式后,编译器会自动生成一个类,在该类中对 () 运算符进行重载,实际 lambda 函数体的实现就是这个仿函数 operator() 的实现,在调用 lambda 表达式时,参数列表和捕获列表的参数,最终都传递给了仿函数的 operator()。

aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们。


举报

相关推荐

0 条评论