天天看點

C++11 在析構函數中執行lambda表達式(std::function)捕獲this指針的陷阱

lambda表達式是C++11最重要也最常用的一個特性之一。lambda來源于函數式程式設計的概念,也是現代程式設計語言的一個特點。

關于lambda表達式的概念并不是本文的重點,網上可以找到無數的寫得極好的文章介紹它。我想說的是善用lambda表達式,将給C++程式設計帶來極大的便利,這是本人最近學習C++11以來真實深切的感受,但是有時候誤用lambda表達式也會給程式設計帶來極大的隐患,本文以最近的經曆說明lambda表達式在使用上的一例陷阱。

一個簡單的例子

下面是一段很簡單的lambda測試代碼。總體的功能就是讓對象在析構時執行指定的

std::function<void(int)>

函數對象。

test_lambda_base

類的功能很簡單,就是在析構函數中執行構造函數傳入的一個

std::function<void()>

對象。

test_lambda

test_lambda_base

的子類,也很簡單,在構造函數中将傳入的

std::function<void(int)>

用lambda表達式封裝成

std::function<void()>

傳給父類

test_lambda_base

構造函數。這樣,當

test_lambda

的對象在析構時将會執行對象構造時指定的

std::function<void(int)>

對象。

#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

。這就是異常發生的直接原因。。。

C++11 在析構函數中執行lambda表達式(std::function)捕獲this指針的陷阱

一開始我總是在糾結為什麼gcc和vs2015下運作的結果不一樣,既然在gcc下運作正常說明我的代碼邏輯沒問題,這該不會是vs2015的一個bug吧?想想也不太可能。還得從代碼上找原因。

将上圖箭頭位置的lambda表達式的捕獲清單改為[=],[&],都試過了,問題依舊:gcc下正常,vs2015下異常。

[=] {
        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

已經是無效的了。

C++11 在析構函數中執行lambda表達式(std::function)捕獲this指針的陷阱

是以前面不論将捕獲清單改為

[&]

還是

[=]

,還是别的什麼嘗試都無濟于事。因為問題的原因不是lambda表達捕獲的

this

指針不對,而是在基類的析構函數中,lambda表達式所捕獲的this指針所指向的子類對象部分的資料已經無效,不可引用了。

解決問題

解決這個問題的辦法很多種,

總的原則就是:如果要在析構函數中調用lambda表達,就要避免lambda使用類成員變量,

對于這個例子,最簡單的辦法就是修改

test_lambda

構造函數,如下示例,改為将

f

參數加入lambda表達捕獲清單,也就是以傳值方式把

f

參數提供給lambda表達。

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

并不是子類對象中已經析構的那個無效對象了。

test_lambda(std::function<void(int)> f):fun(f)
        ,test_lambda_base([this] {
        fun(12345);//gcc下,這個fun已經不是test_lambda中的fun對象了
    })
    {
    }           

複制

是以這代碼在gcc下能正常運作算是僥幸。

總結

如果在基類的析構函數中執行子類提供lambda表達式,lambda表達式中要避免使用子類中類成員變量。因為這時子類的類成員變量已經被析構了,但是子類中的指針類型、基本資料類型變量因為不存在析構的問題是以還是可以用的。