Lambda 表達式語法
Lambda 表達式完整的格式如下:
[捕獲列表] (形參列表) mutable 異常列表-> 返回類型
{
函數體
}
各項的含義:
- 捕獲列表:捕獲外部變量,捕獲的變量可以在函數體中使用,可以省略,即不捕獲外部變量。
- 形參列表:和普通函數的形參列表一樣。可省略,即無參數列表
- mutable:mutable 關鍵字,如果有,則表示在函數體中可以修改捕獲變量,根據具體需求決定是否需要省略。
- 異常列表:noexcept / throw(...),和普通函數的異常列表一樣,可省略,即代表可能拋出任何類型的異常。
- 返回類型:和函數的返回類型一樣。可省略,如省略,編譯器將自動推導返回類型。
- 函數體:代碼實現。可省略,但是沒意義。
使用示例
void LambdaDemo()
{
int a = 1;
int b = 2;
auto lambda = [a, b](int x, int y)mutable throw() -> bool
{
return a + b > x + y;
};
bool ret = lambda(3, 4);
}
編譯器實現原理
編譯器實現 lambda 表達式大致分為一下幾個步驟
- 創建 lambda 類,實現構造函數,使用 lambda 表達式的函數體重載 operator()(所以 lambda 表達式 也叫匿名函數對象)
- 創建 lambda 對象
- 通過對象調用 operator()
編譯器將 lambda 表達式翻譯后的代碼:
class lambda_xxxx
{
private:
int a;
int b;
public:
lambda_xxxx(int _a, int _b) :a(_a), b(_b)
{
}
bool operator()(int x, int y) throw()
{
return a + b > x + y;
}
};
void LambdaDemo()
{
int a = 1;
int b = 2;
lambda_xxxx lambda = lambda_xxxx(a, b);
bool ret = lambda.operator()(3, 4);
}
其中,類名 lambda_xxxx 的 xxxx 是為了防止命名沖突加上的。
lambda_xxxx 與 lambda 表達式 的對應關系
- lambda 表達式中的捕獲列表,對應 lambda_xxxx 類的 private 成員
- lambda 表達式中的形參列表,對應 lambda_xxxx 類成員函數 operator() 的形參列表
- lambda 表達式中的 mutable,對應 lambda_xxxx 類成員函數 operator() 的常屬性 const,即是否是 常成員函數
- lambda 表達式中的返回類型,對應 lambda_xxxx 類成員函數 operator() 的返回類型
- lambda 表達式中的函數體,對應 lambda_xxxx 類成員函數 operator() 的函數體
另外,lambda 表達 捕獲列表的捕獲方式,也影響 對應 lambda_xxxx 類的 private 成員 的類型
- 值捕獲:private 成員 的類型與捕獲變量的類型一致
- 引用捕獲:private 成員 的類型是捕獲變量的引用類型
不捕獲任何外部變量
如果 lambda 表達式不捕獲任何外部變量,在特定的情況下,會有額外的代碼生成。
其中,特定情況是指:有 lambda_xxxx 類 到 函數指針 的類型轉換
如以下代碼
typedef int(_stdcall *Func)(int);
int Test(Func func)
{
return func(1);
}
void LambdaDemo()
{
Test([](int i) {
return i;
});
}
Test 函數接受一個函數指針作為參數,並調用這個函數指針。
實際調用 Test 時,傳入的參數卻是一個 Lambda 表達式,所以這里有一個類型的隱式轉換
lambda_xxxx => 函數指針。
上面已經提到,Lambda 表達式就是一個 lambda_xxxx 類的匿名對象,與函數指針之間按理說不應該存在轉換,但是上述代碼卻沒有問題。
其問題關鍵在於,上述代碼中,lambda 表達式沒有捕獲任何外部變量,即 lambda_xxxx 類沒有任何成員變量,在 operator() 中也就不會用到任何成員變量,也就是說,operator() 雖然是個成員函數,它卻不依賴 this 就可以調用。
因為不依賴 this,所以 一個 lambda_xxxx 類的匿名對象與函數指針之間就存在轉換的可能。
大致過程如下:
- 在 lambda_xxxx 類中生成一個靜態函數,靜態函數的函數簽名與 operator() 一致,在這個靜態函數中,通過一個空指針去調用該類的 operator()
2.在 lambda_xxxx 重載與函數指針的類型轉換操作符,在這個函數中,返回第 1 步中靜態函數的地址。
上述代碼在編譯器的翻譯后代碼如下:
typedef int(_stdcall *Func)(int);
class lambda_xxxx
{
private:
//沒有捕獲任何外部變量,所有沒有成員
public:
/*...省略其他代碼...*/
int operator()(int i)
{
return i;
}
static int _stdcall lambda_invoker_stdcall(int i)
{
return ((lambda_xxxx *)nullptr)->operator()(i);
}
operator Func() const
{
return &lambda_invoker_stdcall;
}
};
int Test(Func func)
{
return func(1);
}
void LambdaDemo()
{
auto lambda = lambda_xxxx ();
Func func = lambda.operator Func();
Test(func);
}
上述代碼只是以 __stdcall 調用約定的函數指針舉例,實際使用時,對於不同調用約定,會生成對應版本的靜態函數和類型轉換函數
以上結論的正確性可以通過顯式調用 lambda 的轉換函數與反匯編來證明
void LambdaDemo()
{
auto lambda = [](int i) {return i;};
Func func = lambda.operator Func();
Test(func);
}
- 轉為函數指針
738 x 691221 x 114
- 將靜態函數 lambda_invoker_stdcall 地址作為 類型轉換函數的返回值
- 靜態函數 lambda_invoker_stdcall 中,使用 0 作為 this 調用 operator()