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++ 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++闭包

lambda表达式使用及缺陷分析展示_谢白羽的博客-CSDN博客_c++ lambda表达式优缺点

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

c++11~c++20 -07-使用lambda注意点_发如雪-ty的博客-CSDN博客 

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++11引入了lambda表达式,它是一种内嵌的匿名函数,可以替代独立函数或函数对象,并且使代码更可读。lambda表达式的基本语法如下: [捕获列表](参数列表) -> 返回类型 {函数体} 其中,捕获列表可以为空或包含一个或多个捕获项,用于在lambda函数中访问外部变量。参数列表是lambda函数的参数,可以为空或包含一个或多个参数。返回类型是可选的,如果不指定,则返回类型会根据函数体中的表达式自动推导。函数体是lambda函数的具体实现。通过lambda表达式,我们可以方便地定义和使用简单的函数对象,提高代码的可读性和灵活性。 下面是一个示例代码,演示了lambda表达式使用: #include <iostream> using namespace std; int main() { auto a = [] { cout << "A" << endl; }; auto b = [] { cout << "B" << endl; }; a(); // 输出 A b(); // 输出 B return 0; } 在这个示例中,我们定义了两个lambda函数对象a和b,分别输出"A"和"B"。通过调用a()和b(),可以执行lambda函数并输出相应的结果。需要注意的是,lambda函数是可以无法赋值的,但可以生成副本。 所以,使用lambda表达式可以方便地定义匿名函数,并且使代码更简洁、可读。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++lambda表达式的编译器实现原理](https://download.csdn.net/download/weixin_38547421/12786127)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [c++lambda表达式用法](https://blog.csdn.net/e21105834/article/details/117064461)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值