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

2022-08-15 14:33:26 浏览数 (1)

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

Lambda介绍

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

崩溃举例

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

示例一:

代码语言:javascript复制
// 示例一
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;
    });
}

示例二: 

代码语言:javascript复制
// 示例二
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"));
                 }
             });
        }
    });
}

示例三:

代码语言:javascript复制
// 示例三
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以值传递的方式捕获进来。

代码语言:javascript复制
// 示例一
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 Lambda表达式详解_lucky-wz的博客-CSDN博客_c lambda表达式

C 笔记-lambda表达式需要注意的地方_IT1995的博客-CSDN博客

浅谈c 中的Lambda表达式_网格小生的博客-CSDN博客

C lambda表达式_悲伤土豆拌饭的博客-CSDN博客_c lambda 表达式

C 11:lambda表达式的陷阱_zzhongcy的博客-CSDN博客_c lambda 异常

关于 c lambda 函数需要注意的点_风竹夜的博客-CSDN博客

C 从Lambda的使用到对C 闭包语法的理解/Lambda的坑_WhiteTian的博客-CSDN博客_c 闭包

c 14新特性_C 17新特性_杨佶Kulbear的博客-CSDN博客

0 人点赞