想要搶先看後面的章節?打賞本文10元,即可獲得帶插圖全本下載地址!

打賞完成記得私信我哦 :p

11.3.2 Lambda表達式的定義與使用

在上面的例子中,我們已經見識過了Lambda表達式的簡潔。具體而言,在C++中定義一個Lambda表達式的語法格式如下:

[變數使用說明符號](參數列表) -> 返回值數據類型
{
// 函數體
}

其中,中括弧「[ ]」表示Lambda表達式的開始,用來告訴編譯器接下來的代碼就是Lambda表達式。在中括弧中,可以指定Lambda表達式對當前作用域(也就是Lambda所在的大括弧「{}」範圍)中的變數的捕捉方式,所以這個中括弧也可以稱為「捕捉列表(capture list)」。如果我們希望在Lambda表達式內部以傳值(複製)的方式使用當前作用域中的所有變數,則使用「[=]」表示,這就意味著Lambda表達式內訪問到的變數只是外部同名變數的一個副本,在Lambda表達式內部對變數的修改不會影響到外部的同名變數。換句話說,也就是在Lambda內部只能讀取外部變數的值但卻沒法對其進行修改。如果試圖修改,將導致一個編譯錯誤。如果中括弧留空,默認情況下也表示以傳值方式使用Lambda表達式外部的變數。例如:

vector<int> v = {51,82,73,44,58};
int nAdd = 10;
// 為容器中小於60的分數加上10分
for_each(v.begin(), v.end(),
// 「[=]」表示以傳值的方式使用Lambda外部的變數
// 因為要修改容器中的數據,所以參數採用引用形式
[=](int& x)
{
nAdd = 20; // 試圖修改外部變數會導致編譯錯誤
if(x < 60)
{
x += nAdd; // 只讀訪問nAdd
}

});

如果想在Lambda表達式內部對外部變數進行修改,則可以使用「[&]」代替「[=]」作為Lambad表達式的開始,這表示Lambda表達式將以傳引用的方式捕捉當前作用域內的變數。這就意味著Lambda表達式內部的變數都是外部同名變數的引用,所以在Lambda表達式中對這些變數的修改將直接影響到當前作用域中變數本身。例如:

int nTotal = 0;
for_each(v.begin(), v.end(),
[&](int x) // 「[&]」表示以傳引用的方式使用Lambda外部的變數
{
nTotal += x; // 修改變數的值
});
cout<<"容器中數據的總和是:"<<nTotal<<endl;

這裡,在Lambda表達式內部,我們將容器中的數據累加到nTotal變數上,因為Lambda表達式是以傳引用的形式來捕捉外部變數,所以這個累加實際上操作的是Lambda表達式外部的局部變數nTotal。這樣,也就實現了數據從Lambda表達式內部向外部的傳遞,最後我們可以看到統計結果。

如果需要與Lambda表達式傳遞多個數據,而同時各個數據的傳遞方式又各不相同,那麼可以在中括弧中的第一個位置用「&」作為Lambda表達式的默認傳遞方式,而那些需要以傳值方式進行傳遞的變數,則可以單獨在中括弧中列出。例如,我們希望在統計成績的同時修正成績:

int nAdd = 10;
int nTotal = 0;
for_each(v.begin(), v.end(),
[&, nAdd](int& x) // 默認採用傳引用訪問,nAdd使用傳值訪問
{
if( x < 60 )
{
x += nAdd; // 傳值訪問nAdd,只能讀取
}
nTotal += x; // 默認採用傳引用訪問nTotal,可以寫入
});
cout<<"容器中所有數據的總和是"<<nTotal<<endl;

在Lambda表達式用「[]」中括弧對捕捉變數的方式進行說明後,就是它的參數列表。它的參數列表跟普通函數的參數列表類似,主要用於接收STL演算法傳遞進來的數據,所以其參數的個數由具體的演算法決定,而參數的類型則由容器中所保存數據的類型決定,至於參數的傳遞形式,到底是傳值還是傳引用,則由我們是否需要修改容器中的數據決定。例如,上面的Lambda表達式應用在for_each()演算法中,這就決定了它只能有一個參數,用以接收演算法傳遞給它的單個數據;而容器中保存的是int類型的數據,所以參數的類型也應該是int類型。至於參數的傳遞方式,如果我們需要修改容器中的數據,就使用傳引用方式(比如第一個例子),否則直接使用傳值方式(比如第二個例子)。

在完成參數列表的定義後,接下來就是Lambda表達式的返回值類型了。在大多數情況下,Lambda表達式所表達的只是對數據的簡單處理,不需要返回值,這時可以省略返回值類型的定義。如果某些演算法需要Lambda表達式有返回值,則可以在參數列表後使用「->」符號來定義它的返回值類型。例如,count_if()演算法就要求與之配合的函數具有bool類型的返回值以決定當前數據是否需要統計在內,所以,當Lambda表達式應用在count_if()演算法中時,就需要定義返回值類型:

// 統計容器中的及格分數int nPass = count_if(vecScore.begin(),vecScore.end(),
[=](int x) -> bool // 定義Lambda表達式的返回值類型為bool類型
{
// 判斷分數是否及格
return x > 60;
});

這裡,我們定義Lambda表達式的返回值為bool類型,這樣在count_if()演算法的執行的過程中,它會逐個地將容器中的數據傳遞給Lambda表達式進行判斷。如果x大於60,也就是「x > 60」的值為true,Lambda表達式返回true,也就表示這個數據符合統計條件應該統計在內。反之,Lambda表達式返回false,表示當前數據不應統計在內。count_if()演算法正是根據Lambda表達式的返回值來判斷當前數據是否應該統計在內。從這裡我們可以看到,Lambda表達式的定義非常靈活,沒有函數名,返回值類型也可有可無。另外它的使用也非常方便,它可以以多種方式捕捉當前區域的變數,省去了函數調用過程中的數據傳遞。同時,定義它的位置也就是使用它的位置,「所見即所得」。所以,Lambda特別適合於運用在STL演算法中,用以表達那些對數據的簡單操作。例如上面的例子,使用Lambda表達式,短短几行代碼就可以實現,極盡「優雅」。而如果要使用函數指針或者函數對象來實現,少則十幾行代碼,多則幾十行代碼,把簡單的事情搞得異常複雜,又何談「優雅」?所以,在那些只需要對數據進行簡單操作的地方,讓函數換上Lambda表達式這件馬甲就對了。

知道更多:定義可以使用Lambda表達式的函數

在上面的例子中,我們都是將Lambda表達式應用在STL演算法中以表達對數據的處理過程,既然Lambda表達式這麼好用,那麼,我們是否也可以定義自己的可以接受Lambda表達式的函數呢?答案是肯定的。在C++中,我們使用STL中的function類模版來表示一個函數。當我們以某種返回值類型以及參數個數和類型特化這個類模板後,得到的是可以代表相應類型函數的模板類。然後使用這個模板類類型作為函數的參數類型,這個函數就可以接受擁有相應返回值和參數的函數指針或者函數對象為實際參數,進而可以在函數內部調用這些通過function類型參數傳遞進來的函數以實現對函數的自定義。從本質上講,Lambda表達式的實質也是一個函數。既然function類型的參數可以接受函數指針和函數對象,自然也可以接受相應類型的Lambda表達式。

例如,在前面的例子中我們實現了一個mycount_if()演算法,它可以接受一個函數指針作為參數,下面我們就用function類模板對它進行改寫,使其不僅可以接受函數指針為參數,同時還可以接受相應類型的函數對象或Lambda表達式為參數:_

#include <functional> // 引入function類模板所在的頭文件

// 可以接受Lambda表達式的mycount_if()演算法
int mycount_if(const vector<int>& v, // 需要統計的容器
// 將函數指針類型更換為function<bool(int)>類型,
// 表示它可以接受一個返回值為bool類型,同時擁有一個int類型參數
// 的函數指針或函數對象,自然也可以是相應類型的Lambda表達式
function<bool(int)> is)
{
// 函數體無需進行任何修改…
}
// …
// 在mycount_if()演算法中應用Lambda表達式
int nPass = mycount_if(vecScore,
// 一個返回值為bool類型,同時擁有一個int類型參數的Lambda表達式
[=](int x) -> bool
{
return x >= 60; // 判斷分數是否及格
});

藉助function類模板,我們也能定義可以使用Lambda表達式的函數,從而將函數的部分業務邏輯留給函數的使用者去靈活地實現,使之可以適應更多的需求,大大地增加了函數的通用性。

總結起來,函數的這三件馬甲:西裝——指向普通函數的函數指針、貂皮大衣——函數對象以及襯衣——Lambda表達式,在本質上,它們都是函數,雖然它們的表現形式不同,但它們所表達的都是一個對數據的處理過程,都可以接收數據,然後對數據進行處理並最終返回結果數據。就像人們總是在春秋穿西裝,而在冬天穿貂皮大衣在夏天穿襯衣一樣,不同的函數應用場景也要求函數換上不同的馬甲。一般而言,普通函數最為常見,主要用以表達一個純粹的數據處理過程。而如果這個數據處理過程需要記住一些過程中的狀態數據,就該換上擁有成員變數的函數對象這件馬甲。而如果這個數據處理過程非常簡單同時又不需要反覆使用,則應該換上簡單到連名字都沒有的Lambda表達式這件馬甲。看天穿衣,是人的基本常識;根據應用場景的不同選擇合適的函數馬甲,卻是程序員的智慧。

圖11-2 函數的三件馬甲


推薦閱讀:
相关文章