如何在 C++11 中使用 Lambda 表达式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在 C++11 中使用 Lambda 表达式相关的知识,希望对你有一定的参考价值。

C++11 的 lambda 表达式规范如下:

[ capture ] ( params ) mutable exception attribute -> ret body
(1)

[ capture ] ( params ) -> ret body
(2)

[ capture ] ( params ) body
(3)

[ capture ] body
(4)

其中

(1) 是完整的 lambda 表达式形式,
(2) const 类型的 lambda 表达式,该类型的表达式不能改捕获("capture")列表中的值。
(3)省略了返回值类型的 lambda 表达式,但是该 lambda 表达式的返回类型可以按照下列规则推演出来:
如果 lambda 代码块中包含了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
如果没有 return 语句,则类似 void f(...) 函数。
省略了参数列表,类似于无参函数 f()。

mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。

exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f() throw(X, Y)。

attribute 用来声明属性。

另外,capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:

[a,&b] a变量以值的方式呗捕获,b以引用的方式被捕获。
[this] 以值的方式捕获 this 指针。
[&] 以引用的方式捕获所有的外部自动变量。
[=] 以值的方式捕获所有的外部自动变量。
[] 不捕获外部的任何变量。

此外,params 指定 lambda 表达式的参数。

一个具体的 C++11 lambda 表达式例子:

#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

int main()

std::vector<int> c 1,2,3,4,5,6,7 ;
int x = 5;
c.erase(std::remove_if(c.begin(), c.end(), [x](int n) return n < x; ), c.end());

std::cout << "c: ";
for (auto i: c)
std::cout << i << ' ';

std::cout << '\n';

// the type of a closure cannot be named, but can be inferred with auto
auto func1 = [](int i) return i+4; ;
std::cout << "func1: " << func1(6) << '\n';

// like all callable objects, closures can be captured in std::function
// (this may incur unnecessary overhead)
std::function<int(int)> func2 = [](int i) return i+4; ;
std::cout << "func2: " << func2(6) << '\n';
参考技术A (1) 是完整的 lambda 表达式形式,
(2) const 类型的 lambda 表达式,该类型的表达式不能改捕获("capture")列表中的值。
(3)省略了返回值类型的 lambda 表达式,但是该 lambda 表达式的返回类型可以按照下列规则推演出来:
如果 lambda 代码块中包含了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
如果没有 return 语句,则类似 void f(...) 函数。
省略了参数列表,类似于无参函数 f()。
参考技术B https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions

c++的lambda使用注意事项,可能导致的崩溃问题分析


Lambda表达式是现代C++的一个语法糖,挺好用的。但是如果使用不当,会导致内存泄露或潜在的崩溃问题。这里总结下Lambda表达式的使用注意事项,避免在使用中的一些陷阱。

Lambda介绍

“Lambda表达式是现代C++在C ++ 11和更高版本中的一个新的语法糖 ,在C++11、C++14、C++17和C++20中Lambda表达的内容还在不断更新。 lambda表达式(也称为lambda函数)是在调用或作为函数参数传递的位置处定义匿名函数对象的便捷方法。通常,lambda用于封装传递给算法或异步方法的几行代码 。

崩溃举例

请看以下示例,会导致崩溃吗?

示例一:

// 示例一
void MainWindow::on_cb_1_currentIndexChanged(const QString &arg1)

qDebug() << "on_cb_1_currentIndexChanged:"<<arg1;

QFuture<void> future = QtConcurrent::run([&]()
QList<QHash<QString,QString>> data;
QString where = QString("Input_date = %1").arg(arg1);
qDebug() <<"where:"<<where;
);

示例二: 

// 示例二
void MainWindow::on_cb_1_clicked()

ui->tb->append("on_cb_1_clicked");

QFuture<void> future = QtConcurrent::run([&]()
QList<QHash<QString,QString>> data;
db->getData("tb_block",data);
qDebug() << "size:"<<data.size();
if(data.size() > 0)
qDebug() << "size:"<<data.size();
QMetaObject::invokeMethod(qApp, [&]
ui->cb_1->clear();
for(auto &lt:data)
ui->cb_1->addItem(lt.value("Input_Date"));

);

);

示例三:

// 示例三
using FilterContainer = std::vector<std::function<bool(int)>>;

FilterContainer filters; // 含有过滤函数的容器

void addDivisorFilter()

auto divisor = 5;

filters.emplace_back(
[&](int value) return value % divisor == 0; // 危险!对divisor的引用会空悬
);

崩溃原因分析

先说结论吧,以上三个示例均会导致崩溃。崩溃原因分析:

示例一,崩溃在QtConcurrent::run开启的线程里访问了arg1。

这个Lambda表达式写法中,使用的是引用捕获[&],当事件处理on_cb_1_currentIndexChanged结束后,在线程里还引用使用了arg1这个参数,而这个agr1的引用已经失效了,这时候线程里还去使用它,导致了崩溃。

示例二,崩溃原因同示例一。局部变量data,尽管QList容器空间是在堆上分配的,但data这个变量分配在栈上。在QMetaObject::invokeMethod开启的Lambda表达式中,同样是使用的[&],引用捕获。当临时变量data失效时,在invokeMethod中仍使用了这个变量data的引用(悬空引用问题),导致了崩溃。

示例三,lambda引用了局部变量​​divisor​​​, 但是局部变量的生命期在​​addDivisorFilter​​​返回时终止,也就是在​​filters.emplace_back​​返回之后,所以添加到容器的函数本质上就像是一到达容器就死亡了,使用那个过滤器会产生未定义行为。

以上示例崩溃的原因都可以归结为使用了悬空引用。需要特别注意悬空引用。

悬空引用

引用捕获会导致闭包包含一个局部变量的引用或者一个形参的引用(在定义lamda的作用域)。如果一个由lambda创建的闭包的生命期超过了局部变量或者形参的生命期,那么闭包的引用将会空悬。

正确写法

正确的写法如下:

需要把arg1和data以值传递的方式捕获进来。

// 示例一
void MainWindow::on_cb_1_currentIndexChanged(const QString &arg1)

qDebug() << "on_cb_1_currentIndexChanged:"<<arg1;

QFuture<void> future = QtConcurrent::run([&,arg1]()
QList<QHash<QString,QString>> data;
QString where = QString("Input_date = %1").arg(arg1);
qDebug() <<"where:"<<where;
);


// 示例二
void MainWindow::on_cb_1_clicked()

ui->tb->append("on_cb_1_clicked");

QFuture<void> future = QtConcurrent::run([&]()
QList<QHash<QString,QString>> data;
db->getData("tb_block",data);
qDebug() << "size:"<<data.size();
if(data.size() > 0)
qDebug() << "size:"<<data.size();
QMetaObject::invokeMethod(qApp, [&,data]
ui->cb_1->clear();
for(auto &lt:data)
ui->cb_1->addItem(lt.value("Input_Date"));

);

);


// 示例三
using FilterContainer = std::vector<std::function<bool(int)>>;

FilterContainer filters; // 含有过滤函数的容器

void addDivisorFilter()

auto divisor = 5;

filters.emplace_back(
[&,divisor](int value) return value % divisor == 0;
);

注意事项

使用Lambda表达式的一些注意事项:

1、使用到外部引用要小心谨慎,避免悬空引用。

若需要用到的外部局部变量,需以值传递的方式捕获而非引用捕获(若是外部指针变量则需深拷贝)。

2、谨慎使用或者不用外部指针。

如果你用值捕获了个指针,你在lambda创建的闭包中持有这个指针的拷贝,但你不能阻止lambda外面的代码删除指针指向的内容,从而导致你拷贝的指针空悬。

3、注意引用捕获陷阱:引用捕获[&]不要使用外部局部变量。

4、注意this陷阱:lambda里避免有全局变量或静态变量或者比当前类生命周期更长的变量。Effective Modern C++ 条款31 对于lambda表达式,避免使用默认捕获模式

5、避免使用默认捕获模式((即“[=]”或“[&]”,它可能导致你看不出悬空引用问题)。

默认值捕获就意外地捕获了this指针,而不是你以为的外部变量。

在C++14中,捕获成员变量一种更好的方法是使用广义lambda捕获(generalized lambda capture,即,捕获语句可以是表达式[x= x],条款32)。

6、注意捕获的是可见(在创建lambda的作用域可见)的非static局部变量(包含形参)。

每一个非static成员函数都有一个this指针,然后每当你使用类的成员变量时都用到这个指针。这时候lambda闭包的活性与Widget对象的生命期有紧密关系,闭包内含有Widget的this指针的拷贝。

正常情况下,lambda表达式中访问类的对象成员变量需要捕获this,但是这里捕获的是this指针,指向的是对象的引用,正常情况下可能没问题,但是如果多线程情况下,函数的作用域超过了对象的作用域,对象已经被析构了,还访问了成员变量,就会有问题。

好在C++17增加了新特性可以捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关,使用上就安全一些。

引用


以上是关于如何在 C++11 中使用 Lambda 表达式的主要内容,如果未能解决你的问题,请参考以下文章

c++的lambda使用注意事项,可能导致的崩溃问题分析

如何使用lambda表达式捕获局部变量?

一文读懂C++11的Lambda表达式的用法与原理

什么是 C++11 中的 lambda 表达式?

C++11- lambda表达式

C++11 ——— lambda表达式