C++11 在析构函数中执行lambda表达式(std::function)捕获this指针的陷阱

2022-05-07 10:10:59 浏览数 (1)

lambda表达式是C 11最重要也最常用的一个特性之一。lambda来源于函数式编程的概念,也是现代编程语言的一个特点。 关于lambda表达式的概念并不是本文的重点,网上可以找到无数的写得极好的文章介绍它。我想说的是善用lambda表达式,将给C 编程带来极大的便利,这是本人最近学习C 11以来真实深切的感受,但是有时候误用lambda表达式也会给编程带来极大的隐患,本文以最近的经历说明lambda表达式在使用上的一例陷阱。

一个简单的例子

下面是一段很简单的lambda测试代码。总体的功能就是让对象在析构时执行指定的std::function<void(int)>函数对象。 test_lambda_base 类的功能很简单,就是在析构函数中执行构造函数传入的一个std::function<void()>对象。 test_lambdatest_lambda_base的子类,也很简单,在构造函数中将传入的std::function<void(int)>用lambda表达式封装成std::function<void()>传给父类test_lambda_base构造函数。这样,当test_lambda的对象在析构时将会执行对象构造时指定的std::function<void(int)>对象。

代码语言:javascript复制
#include <iostream>
#include <functional>
using namespace std;
class test_lambda_base {
public:
    test_lambda_base(std::function<void()> f):on_release(f) {       
    }
    ~test_lambda_base() {
        cout << "destructor of test_lambda_base" << endl;
        on_release();  //执行传入的函数对象
    }
private:
    std::function<void()> on_release;
};
class test_lambda:public test_lambda_base {
public:
    test_lambda(std::function<void(int)> f):fun(f)
        ,test_lambda_base([this] {
        fun(12345);
    })
    {
    }
    ~test_lambda() {
        cout << "destructor of test_lambda" << endl;
    }
private:
    std::function<void(int)> fun;
};
int main() {
    test_lambda tst_lam([](int i){
        cout<<i<<endl;
    });
    cout << "!! !Hello World!!!" << endl; // prints !!!Hello World!!!
}

在eclipse gcc(5.2)环境下编译运行,的确会输出预期的运行结果,程序结束的时候,调用了指定的lambda表达式:

!! !Hello World!!! destructor of test_lambda destructor of test_lambda_base 12345

问题来了

一切都是预期的。。。完美。。。 然而当我在VisualStudio2015下同样运行这段代码,却抛出了异常。。。仔细跟踪分析,发现当程序到下图箭头所指的位置时,test_lambda的成员变量fun显示是empty。这就是异常发生的直接原因。。。

一开始我总是在纠结为什么gcc和vs2015下运行的结果不一样,既然在gcc下运行正常说明我的代码逻辑没问题,这该不会是vs2015的一个bug吧?想想也不太可能。还得从代码上找原因。 将上图箭头位置的lambda表达式的捕获列表改为[=],[&],都试过了,问题依旧:gcc下正常,vs2015下异常。

代码语言:javascript复制
[=] {
        fun(12345);
    };
[&] {
        fun(12345);
    };

析构顺序

然后我想到了C 析构顺序的问题,按照C 标准,C 对象析构的顺序与构造顺序完全相反:

析构函数体->清除成员变量->析构基类部分(从右到左)->析构虚基类部分

所以上面代码中在test_lambda_base的析构函数中执行子类test_lambda的成员变量fun时,fun作为一个std::function对象已经被析构清除了,这时fun已经是个无效变量,执行它当然会抛出异常。 为了证实这个判断,打开头文件#include <functional>找到function的析构函数,如下图在析构函数上设置一个调试断点,再运行程序到断点处。 看下图中的”调用堆栈”窗口。在test_lambda的析构函数~test_lambda执行时,类型为std::function<void(int)>fun成员的析构函数~function<void(int)>()被执行了,所以当再执行到test_lambda_base的析构函数时,fun已经是无效的了。

所以前面不论将捕获列表改为[&]还是[=],还是别的什么尝试都无济于事。因为问题的原因不是lambda表达捕获的this指针不对,而是在基类的析构函数中,lambda表达式所捕获的this指针所指向的子类对象部分的数据已经无效,不可引用了。

解决问题

解决这个问题的办法很多种, 总的原则就是:如果要在析构函数中调用lambda表达,就要避免lambda使用类成员变量, 对于这个例子,最简单的办法就是修改test_lambda构造函数,如下示例,改为将f参数加入lambda表达捕获列表,也就是以传值方式把f参数提供给lambda表达。

代码语言:javascript复制
    test_lambda(std::function<void(int)> f):fun(f)
        ,test_lambda_base([f] {
        f(12345);
    })
    {
    }

为什么gcc和vs2015下代码的表现不同?

最后一个问题:为什么gcc和vs2015下代码的表现不同?

我同样用前面在std::function析构函数加断点的方式在eclipse gcc环境下做了测试,测试结果表明gcc也是按C 标准顺序执行对象析构的,但不同的是gcc在构造下面这个lambda表达式时,将fun对象复制了一份,所以当代码执行到lambda表达式时,fun并不是子类对象中已经析构的那个无效对象了。

代码语言:javascript复制
    test_lambda(std::function<void(int)> f):fun(f)
        ,test_lambda_base([this] {
        fun(12345);//gcc下,这个fun已经不是test_lambda中的fun对象了
    })
    {
    }

所以这代码在gcc下能正常运行算是侥幸。

总结

如果在基类的析构函数中执行子类提供lambda表达式,lambda表达式中要避免使用子类中成员变量。因为这时子类的类成员变量已经被析构了,但是子类中的指针类型、基本数据类型变量因为不存在析构的问题所以还是可以用的。

0 人点赞