C++ Lambda 編譯器實現原理


Lambda 表達式語法

Lambda 表達式完整的格式如下:

[捕獲列表] (形參列表) mutable 異常列表-> 返回類型
{
    函數體
}

各項的含義:

  1. 捕獲列表:捕獲外部變量,捕獲的變量可以在函數體中使用,可以省略,即不捕獲外部變量。
  2. 形參列表:和普通函數的形參列表一樣。可省略,即無參數列表
  3. mutable:mutable 關鍵字,如果有,則表示在函數體中可以修改捕獲變量,根據具體需求決定是否需要省略。
  4. 異常列表:noexcept / throw(...),和普通函數的異常列表一樣,可省略,即代表可能拋出任何類型的異常。
  5. 返回類型:和函數的返回類型一樣。可省略,如省略,編譯器將自動推導返回類型。
  6. 函數體:代碼實現。可省略,但是沒意義。

使用示例

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 表達式大致分為一下幾個步驟

  1. 創建 lambda 類,實現構造函數,使用 lambda 表達式的函數體重載 operator()(所以 lambda 表達式 也叫匿名函數對象)
  2. 創建 lambda 對象
  3. 通過對象調用 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 表達式 的對應關系

  1. lambda 表達式中的捕獲列表,對應 lambda_xxxx 類的 private 成員
  2. lambda 表達式中的形參列表,對應 lambda_xxxx 類成員函數 operator() 的形參列表
  3. lambda 表達式中的 mutable,對應 lambda_xxxx 類成員函數 operator() 的常屬性 const,即是否是 常成員函數
  4. lambda 表達式中的返回類型,對應 lambda_xxxx 類成員函數 operator() 的返回類型
  5. lambda 表達式中的函數體,對應 lambda_xxxx 類成員函數 operator() 的函數體

另外,lambda 表達 捕獲列表的捕獲方式,也影響 對應 lambda_xxxx 類的 private 成員 的類型

  1. 值捕獲:private 成員 的類型與捕獲變量的類型一致
  2. 引用捕獲: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 類的匿名對象與函數指針之間就存在轉換的可能。

大致過程如下:

  1. 在 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);
}
  • 轉為函數指針

1png

738 x 691221 x 114

  • 將靜態函數 lambda_invoker_stdcall 地址作為 類型轉換函數的返回值

2png

  • 靜態函數 lambda_invoker_stdcall 中,使用 0 作為 this 調用 operator()

3png


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM