0. 引言
最近和同事讨论了一下C++中lambda函数的使用,因此想着周末总结一下lambda相关知识。
1. lambda的定义(lambda是什么?)
1.1 lambda表达式的定义
lambda函数的背景可以看看相关链接 C++里为什么要添加lambda?是C++本身的什么问题造成的? - 知乎 可能非官方。
下面看一下C++ Primer 给出的lambda表达式的定义
一个lambda表达式表示一个可调用的代码单元。一个表达式具有如下形式
[捕获列表] (参数列表)-> 返回类型 { 函数体 }
譬如我们可以定义一个lambda表达式如下:
auto f = [&]() -> int {return 42;};
1.2 lambda 实现原理
lambda本身是一个函数对象,当编写好lambda表达式后,编译器会将该表达式翻译成一个未命名的类的未命名对象, 且在lambda表达式产生的类中含有一个重载的函数调用运算符。
上面文字如果看起来累,可以看下面一个例子来加深理解。我们定一个lambda表达式如下
auto f = [] (int x) {return 42;};
通过C++ Insights观察到上述代码生成如下的类定义:
class __lambda_6_11
{
public:
inline /*constexpr */ int operator()(int x) const
{
return 42;
}
using retType_6_11 = int (*)(int);
inline /*constexpr */ operator retType_6_11 () const noexcept
{
return __invoke;
};
private:
static inline int __invoke(int x)
{
return 42;
}
};
2. lambda使用总结
lambda函数有两种默认捕获方式:按引用和按值捕获。本文总结一下按引用捕获和按值捕获的缺点,以及给出一个我碰到的很骚气的lambda表示式与pthread_create函数结合的用法并分析为啥可以这样用。
2.1 lambda默认捕获方式
lambda表达式按引用捕获方式使用方法如下:
#include <iostream>
int main() {
int b = 3;
int c = 4;
auto f = [&b] (int x) {return 42;};
return 0;
}
引用捕获一般建议如上使用,在捕获列表中利用 &(捕获的变量名)表示按引用捕获。
lambda表达式按值捕获方式使用如下:
int c = 4;
auto fb = [c] () {return 42 - c;};
2.2 lambda表达式生成的对象的大小探究
首先需要明白,lambda表达式必然会将捕获的值存放在某个地方,个人无聊研究了一下在我机器上lambda表达式占用的内存
我们从下面的例子开始
int a = 0;
int b = 0;
auto fb = [a, b]() {return 42;};
std::cout << sizeof(fb) << std::endl;
分别进行如下三种测试:
auto fb = [a, b]() {return 42;};
auto fb1 = [&a, b]() {return 42;};
auto fb2 = [&a, &b] () {return 42;};
其测试结果如下:
按值捕获所占内存大小:8
按值和引用捕获所占内存大小:16
按引用捕获所占内存大小:16
空捕获所占内存大小:1
因此可以看出,按引用捕获会导致lambda表达式占用内存变大,而空捕获列表的lambda表达式只会占用一个字节
2.3 按默认捕获方式的缺点
按引用捕获方式会导致程序产生未定义行为,当引用对象的生命周期比lambda表达式的生命周期短的时候便可能发生。其例子如下
void func(std::function<int()>& fb) {
std::string a = "hello world";
fb = [&a]() {
std::cout << a << std::endl;
return 42;
};
}
int main() {
std::function<int()> fb;
func(fb);
fb();
return 0;
}
2.4 按值捕获
无论是按引用捕获还是按值捕获,均只能捕获局部变量(初静态变量和全局变量), 按值捕获的一个问题是,当我们想捕获一个类的成员变量时,会出现失败。
class A {
public:
A() : a(0) {}
void func() {
int b = 0;
auto fc = [b]{
std::cout << "捕获a:" << b;
return;};
fc();
}
private:
int a;
};
int main() {
A b;
b.func();
return 0;
}
该程序会输出正确结果,按值捕获b。当我们试图捕获a时,也即lambda表达式变成
auto fc = [a]{
std::cout << "捕获a:" << a;
return;};
此时编译器会报错:
error: 'a' in capture list does not name a variable
error: 'this' cannot be implicitly captured in this context
因此此时我们其实需要捕获this指针,也即lambda表达式变成
auto fc = [this]{
std::cout << "捕获a:" << a;
return;};
此刻便会正确捕获类成员变量。
3. lambda与pthread_create的例子
看下面的实现
class ThreadImpl {
public:
ThreadImpl(std::function<void()> thread_routinue);
int join();
pthread_t threadId();
private:
std::function<void()> _thread_routinue;
pthread_t _thread_handler;
};
ThreadImpl::ThreadImpl(std::function<void()> thread_routinue)
: _thread_routinue(std::move(thread_routinue)) {
auto rc =
pthread_create(
&_thread_handler,
nullptr,
[] (void *arg) -> void* {
static_cast<ThreadImpl*>(arg)->_thread_routinue();
return (void*)1;},
this);
if (rc != 0) {
std::cout << "creat pthread error" << std::endl;
}
}
在ThreadImpl的构造函数中 调用pthread_create传入了四个参数,其中第三个参数为一个lambda表达式,第四个参数为this指针。
上述表达式可以正确工作。原因其实是 lambda表达式会实例化成一个函数,然后作为第三个参数。
4. 总结
lambda表达式其实就是一个类模版,在使用时会被实例化。而且C++14中lambda表达式的使用越来越多。
本文会形成一个系列,具体讲解C++11/14/17/20中lambda表达式的高级特性和相关概念。目前已有系列二可参考