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
。這就是異常發生的直接原因。。。
一開始我總是在糾結為什麼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
已經是無效的了。
是以前面不論将捕獲清單改為
[&]
還是
[=]
,還是别的什麼嘗試都無濟于事。因為問題的原因不是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表達式中要避免使用子類中類成員變量。因為這時子類的類成員變量已經被析構了,但是子類中的指針類型、基本資料類型變量因為不存在析構的問題是以還是可以用的。