C++ Lambda 表示式使用詳解

Yang_Lian發表於2014-09-13

C++ 11 對LB的支援,對於喜歡Functional Programming的人來說,無疑是超好訊息。它使得C++進入了和C#,JavaScript等現代流行的程式設計語言所代表的名人堂。

不熟悉LB本身的網友,可以看MSDN文章

( http://msdn.microsoft.com/en-us/library/dd293608.aspx ),我僅僅簡單地分析一下VC++中LB的用法,實現,和效能。

無名引用

對於一次性的,帶參數列達式,用LB可以節省不必要的class定義和維護,簡化程式的設計-維護代價。

比如下面的vector處理程式碼,簡潔明瞭:

vector<int> v1(10, 1);

int sum = 0;

for_each (v1.begin(), v1.end(), [&](int i){ sum += i; });//Line1

否則,我們必須定義一個function類,把如此簡單的事情複雜化。用了LB,我們把定義function 類的工作,轉交給編譯。VC++中,上述LB編譯的實現是產生一個隱身類:

class  _lambda_a01 {

int &capture1_;

public:

_lambda_a01(int &x): capture1_(x) {}  //Line2

operator void (int i) { capture1_ += I; }

};

在引用時(Line1),它變成:

_lambda_a01 lbd1(sum);

for(auto a:v1){

ldb1(a);

}

讀者也許好奇,為什麼C++不直接把LB轉換成inline expression (inline 表示式),而是要生成一個隱身類呢?這是因為LB的確可以當成“type”變數來用,這樣使得LB和其他類有了同等地位。比如:

vector<int> v1(10, 1);

int sum = 0;

for_each (v1.begin(), v1.end(), [&](int i){ sum += i; });//Line1

vector<int> v2(10, 1);

int sum2 = 0;

for_each (v1.begin(), v1.end(), [&](int i){ sum2 += i; });//Line2

我們如果用上述的方法,Line1Line2重複程式碼,是軟體工程的大忌。我們可以用下列LB使用模式:

有名無型引用

vector<int> v1(10, 1);

vector<int> v2(10, 1);

int sum = 0;

auto lb = [&](int i){ sum += i; };  //Line0

 

for_each (v1.begin(), v1.end(), lb);//Line1

sum = 0;                              // Line1.1

for_each (v1.begin(), v1.end(), lb});//Line2

Line0,我們定義了一個有名(lb)無型的LB,可以在Line1Line2重複使用。

注意的是,

1) 每個LB的“定義”都會產生新的“隱身”類,所以儘量用“有名引用”,會減少程式碼的size,縮小工作集。

2) 定義時,LB一次性“俘獲”環境變數,所以上面修改後的程式碼加了Line1.1,以便正確表達應用邏輯。

3) 俘獲可以是“傳值(by value)”也可以是“傳引用(by reference)。我們Line0用的是by reference.

有名有型引用

上面兩種LB使用模式,是LB應用的主要模式,它直接反映出了LB的優點。另一方面說,既然LB無非是隱身類,我們沒有理由不能把它當作普通變數使用。這個模式是一種簡化的functor使用模式。我們可以把LB定義成一個std::function,比如上面的auto lb可以定義成:

std::function <void(int)> lb; //lb is a function which takes an integer and returns void

注意到用這個定義,使得我們可以推遲給LB變數賦值,甚至一變數賦多址(不同時間)。下面就是一個簡單用例:

struct MyLambda

{

std::function <int (int)> _lbda;//line1

int _extra;

};

 

MyLambda TestLambdaObj(int t)

{

MyLambda ret;

if (t == 1)

{

ret._extra = t;

ret._lbda = [=](int x)  -> int { return t + x; }; //line2

return ret;

}

else

{

ret._extra = t;

ret._lbda = [=](int x)  -> int { return t * x; };//line3

return ret;

}

}

 

void TestLambdaFun2(int t)

{

MyLambda ret = TestLambdaObj(t);

int v = ret._lbda(t);                                //line4

printf(“v is ‘%d’ for type %d”, v, t);

}

我們先定義MyLambda資料類,並與其定義了一了function成員_lbda,根據C++ SPEC,他可以由LB轉換構造,並且和普通的類變數無甚區別。然後我們可以執行時給它賦值(line2line3), 當作普通function來使用(line4)。

注意的是:

  • function的定義中沒有“閉包”的概念,閉包的形成是在LB建立時實現(line2line3)。
  • 把LB賦值給function變數,必然造成呼叫時(line4)的間接性(通過函式指標),其效能相當於虛擬函式,也不能inline化,當然比直接呼叫有所下降。

閉包(closure)是LB的獨特附加值

如果你問為什用LB而不用std::function?我的回答是“閉包”。

C++用LB來實現閉包,是一個簡化繁瑣的class初始化的syntax sugar。這一點是std::function所不可替代的。比如說:

auto sum = 0;

auto step = 2;

auto lb = [&](int i){ sum += i + step; }//capture sum and step by ref

lb形成自己的閉包,自動從環境中俘獲了sumstep,若用class實現,上面的程式起碼增加10行程式碼。

LB效能初探

下面的簡單程式,測試四種功能完全一樣,但使用不同表示式的邏輯:

1)t =1 時用LB,

2)t=2 時用直接表示式

3)t=3 時用函式

4)t=4時用std::function間接呼叫LB

void TestLambdaFun(int t)

{

using namespace std;

vector<int> v1(10, 1);

int x = 0;

int u = 0;

if (t == 1)

{

clock_t begin = clock();

for (int i = 0; i < 100000; ++i)

{

for_each (v1.begin(),

v1.end(),

[&x, &u](int i){ u += i+(x++); });// Line 1

}

clock_t end = clock();

auto spent = double(end – begin) / CLOCKS_PER_SEC;

printf(“spent for type ‘%d’ is %f u is %d\n”, t, spent, u);

}

else if (t == 2)

{

clock_t begin = clock();

for (int i = 0; i < 100000; ++i)

{

auto _First = v1.begin();

auto _Last = v1.end();

for (; _First != _Last; ++_First)

{

u = *_First+(x++);                  // Line 2

}

}

clock_t end = clock();

auto spent = double(end – begin) / CLOCKS_PER_SEC;

printf(“spent for type ‘%d’ is %f u is %d\n”, t, spent, u);

}

else if (t == 3)

{

clock_t begin = clock();

for (int i = 0; i < 100000; ++i)

{

auto _First = v1.begin();

auto _Last = v1.end();

for (; _First != _Last; ++_First)

{

FuncAdd(u, x, *_First);             // Line 3

}

}

clock_t end = clock();

auto spent = double(end – begin) / CLOCKS_PER_SEC;

printf(“spent for type ‘%d’ is %f u is %d\n”, t, spent, u);

}

else if (t == 4)

{

clock_t begin = clock();

std::function <void (int)> lbda;

for (int i = 0; i < 100000; ++i)

{

lbda = [&](int i){ u += i + (x++); };

for_each (v1.begin(), v1.end(), lbda); // Line 4

}

clock_t end = clock();

auto spent = double(end – begin) / CLOCKS_PER_SEC;

printf(“spent for type ‘%d’ is %f u is %d\n”, t, spent, u);

}

 

}

 

void FuncAdd(int &u, int &x, int i)

{

u = i+(x++);

}

下面是VC++ 2010中的測試結果:

  • debug模式下,t=2時速度最快,這是因為t=1,t=3,t=4時都是用了函式呼叫,效能當然不及inline表示式。
  • release模式下(選擇/Ob1優化,對inline函式進行inline擴充套件)
    • t=1和t=2速度完全一樣,比t=3時平均快3倍。當然,我們也可以把FuncAdd inline化。這裡的主要目的,是證明優化後,LB的效能和表示式完全一樣。證明C++ Lambda expression不是浪得虛名的隱身類的syntax sugar,而是名副其實的“表示式”。

t=4最慢,它和t=3類似。但是由於通過了std::function的虛擬函式表間接呼叫,/Ob1優化失去作用,使它不但要呼叫一個() operator,而且是通過“虛擬表”間接呼叫。所以從效能上說,把LB通過std::function間接使用,失去了LB的效能優勢。

總結

C++ 11 的lambda expression(簡稱LB),在可以保證和inline expression同樣效能的條件下,增加了引數功能和閉包功能,是我們寫出簡潔,明瞭,易維護程式碼的絕佳工具。應用時,為了避免程式碼重複和增加隱身類的數量,可用有名無型的LB變數。LB也可以賦值於std::function,當作函式指標使用,但是效能不及簡單地inline使用。

相關文章