1. 程式人生 > >c++ Lambda函式學習

c++ Lambda函式學習

或許,Lambda 表示式算得上是 C++ 11 新增特性中最激動人心的一個。這個全新的特性聽起來很深奧,但卻是很多其他語言早已提供(比如 C#)或者即將提供(比如 Java)的。簡而言之,Lambda 表示式就是用於建立匿名函式的。GCC 4.5.x 和 Microsoft Visual Studio 早已提供了對 lambda 表示式的支援。在 GCC 4.7 中,預設是不開啟 C++ 11 特性的,需要新增 -std=c++11 編譯引數。而 VS2010 則預設開啟。

為什麼說 lambda 表示式如此激動人心呢?舉一個例子。標準 C++ 庫中有一個常用演算法的庫,其中提供了很多演算法函式,比如 sort() 和 find()。這些函式通常需要提供一個“謂詞函式 predicate function”。所謂謂詞函式,就是進行一個操作用的臨時函式。比如 find() 需要一個謂詞,用於查詢元素滿足的條件;能夠滿足謂詞函式的元素才會被查找出來。這樣的謂詞函式,使用臨時的匿名函式,既可以減少函式數量,又會讓程式碼變得清晰易讀。

下面來看一個例子:

#include <algorithm>
#include <cmath>

void abssort(float *x, unsigned N)
{
  std::sort(x,
            x + N,
            [](float a, float b) { return std::abs(a) < std::abs(b); });
}

從上面的例子來看,儘管支援 lambda 表示式,但 C++ 的語法看起來卻很“神奇”。lambda 表示式使用一對方括號作為開始的標識,類似於宣告一個函式,只不過這個函式沒有名字,也就是一個匿名函式。這個匿名函式接受兩個引數,a和b;其返回值是一個 bool 型別的值,注意,返回值是自動推斷的,不需要顯式宣告,不過這是有條件的!條件就是,lambda 表示式的語句只有一個 return。函式的作用是比較 a、b 的絕對值的大小。然後,在此例中,這個 lambda 表示式作為一個閉包被傳遞給 std::sort() 函式。

下面,我們來詳細解釋下這個神奇的語法到底代表著什麼。

我們從另外一個例子開始:

std::cout << [](float f) { return std::abs(f); } (-3.5);

輸出值是什麼?3.5!注意,這是一個函式物件(由 lambda 表示式生成),其實參是 -3.5,返回值是引數的絕對值。lambda 表示式的返回值型別是語言自動推斷的,因為std::abs()的返回值就是 float。注意,前面我們也提到了,只有當 lambda 表示式中的語句“足夠簡單”,才能自動推斷返回值型別。

C++ 11 的這種語法,其實就是匿名函式宣告之後馬上呼叫(否則的話,如果這個匿名函式既不呼叫,又不作為閉包傳遞給其它函式,那麼這個匿名函式就沒有什麼用處)。如果你覺得奇怪,那麼來看看 JavaScript 的這種寫法:

function() {} ();
function(a) {} (-3.5);

C++ 11 的寫法完全類似 JavaScript 的語法。

如果我不想讓 lambda 表示式自動推斷型別,或者是 lambda 表示式的內容很複雜,不能自動推斷怎麼辦?比如,std::abs(float)的返回值是 float,我想把它強制轉型為 int。那麼,此時,我們就必須顯式指定 lambda 表示式返回值的型別:

std::cout << [](float f) -> int { return std::abs(f); } (-3.5);

這個語句與前面的不同之處在於,lambda 表示式的返回時不是 float 而是 int。也就是說,上面語句的輸出值是 3。返回值型別的概念同普通的函式返回值型別是完全一樣的。

當我們想引用一個 lambda 表示式時,我們可以使用auto關鍵字,例如:

auto lambda = [] () -> int { return val * 100; };

auto關鍵字實際會將 lambda 表示式轉換成一種類似於std::function的內部型別(但並不是std::function型別,雖然與std::function“相容”)。所以,我們也可以這麼寫:

std::function<int()> lambda = [] () -> int { return val * 100; };

如果你對std::function這種寫法感到很神奇,可以檢視 C++ 11 的有關std::function的用法。簡單來說,std::function就是一個可呼叫物件模板類,代表一個可呼叫物件,接受 0 個引數,返回值是int。所以,當我們需要一個接受一個double作為引數,返回int的物件時,就可以寫作:std::function。

引入 lambda 表示式的前導符是一對方括號,稱為 lambda 引入符(lambda-introducer)。lambda 引入符是有其自己的作用的,不僅僅是表明一個 lambda 表示式的開始那麼簡單。lambda 表示式可以使用與其相同範圍 scope 內的變數。這個引入符的作用就是表明,其後的 lambda 表示式以何種方式使用(正式的術語是“捕獲”)這些變數(這些變數能夠在 lambda 表示式中被捕獲,其實就是構成了一個閉包)。目前為止,我們看到的僅僅是一個空的方括號,其實,這個引入符是相當靈活的。例如:

float f0 = 1.0;
std::cout << [=](float f) { return f0 + std::abs(f); } (-3.5);

其輸出值是 4.5。[=] 意味著,lambda 表示式以傳值的形式捕獲同範圍內的變數。另外一個例子:

float f0 = 1.0;
std::cout << [&](float f) { return f0 += std::abs(f); } (-3.5);
std::cout << '\n' << f0 << '\n';

輸出值是 4.5 和 4.5。[&] 表明,lambda 表示式以傳引用的方式捕獲外部變數。那麼,下一個例子:

float f0 = 1.0;
std::cout << [=](float f) mutable { return f0 += std::abs(f); } (-3.5);
std::cout << '\n' << f0 << '\n';

這個例子很有趣。首先,[=]意味著,lambda 表示式以傳值的形式捕獲外部變數。C++ 11 標準說,如果以傳值的形式捕獲外部變數,那麼,lambda 體不允許修改外部變數,對 f0 的任何修改都會引發編譯錯誤。但是,注意,我們在 lambda 表示式前聲明瞭mutable關鍵字,這就允許了 lambda 表示式體修改 f0 的值。因此,我們的例子本應報錯,但是由於有 mutable 關鍵字,則不會報錯。那麼,你會覺得輸出值是什麼呢?答案是,4.5 和 1.0。為什麼 f0 還是 1.0?因為我們是傳值的,雖然在 lambda 表示式中對 f0 有了修改,但由於是傳值的,外部的 f0 依然不會被修改。

上面的例子是,所有的變數要麼傳值,要麼傳引用。那麼,是不是有混合機制呢?當然也有!比如下面的例子:

float f0 = 1.0f;
float f1 = 10.0f;
std::cout << [=, &f0](float a) { return f0 += f1 + std::abs(a); } (-3.5);
std::cout << '\n' << f0 << '\n';

這個例子的輸出是 14.5 和 14.5。在這個例子中,f0 通過引用被捕獲,而其它變數,比如 f1 則是通過值被捕獲。

下面我們來總結下所有出現的 lambda 引入符:

[]        // 不捕獲任何外部變數
[=]      // 以值的形式捕獲所有外部變數
[&]      // 以引用形式捕獲所有外部變數
[x, &y] // x 以傳值形式捕獲,y 以引用形式捕獲
[=, &z]// z 以引用形式捕獲,其餘變數以傳值形式捕獲
[&, x]  // x 以值的形式捕獲,其餘變數以引用形式捕獲

另外有一點需要注意。對於[=]或[&]的形式,lambda 表示式可以直接使用 this 指標。但是,對於[]的形式,如果要使用 this 指標,必須顯式傳入:

[this]() { this->someFunc(); }();

至此,我們已經大致瞭解了 C++ 11 提供的 lambda 表示式的概念。建議通過結合 lambda 表示式與std::sort()或std::for_each()這樣的標準函式來嘗試使用一下吧!