目錄結構:c++
lambda表達式是C++11中引入的一項新技術,利用lambda表達式能夠編寫內嵌的匿名函數,用以替換獨立函數或者函數對象,而且使代碼更可讀。可是從本質上來說,lambda表達式只是一種語法糖,由於全部其能完成的工做均可以用其它稍微複雜的代碼來實現。可是它簡便的語法卻給C++帶來了深遠的影響。若是從廣義上說,lamdba表達式產生的是函數對象。在類中,能夠重載函數調用運算符(),此時類的對象能夠將具備相似函數的行爲,咱們稱這些對象爲函數對象(Function Object)或者仿函數(Functor)。相比lambda表達式,函數對象有本身獨特的優點。下面咱們開始具體講解這兩項黑科技。算法
咱們先從簡答的例子開始,咱們定義一個能夠輸出字符串的lambda表達式,表達式通常都是從方括號[]開始,而後結束於花括號{},花括號裏面就像定義函數那樣,包含了lamdba表達式體:編程
// 定義簡單的lambda表達式 auto basicLambda = [] { cout << "Hello, world!" << endl; }; // 調用 basicLambda(); // 輸出:Hello, world!
上面是最簡單的lambda表達式,沒有參數。若是須要參數,那麼就要像函數那樣,放在圓括號裏面,若是有返回值,返回類型要放在->後面,即拖尾返回類型,固然你也能夠忽略返回類型,lambda會幫你自動推斷出返回類型:設計模式
// 指明返回類型 auto add = [](int a, int b) -> int { return a + b; }; // 自動推斷返回類型 auto multiply = [](int a, int b) { return a * b; }; int sum = add(2, 5); // 輸出:7 int product = multiply(2, 5); // 輸出:10
你們可能會想lambda表達式最前面的方括號的意義何在?其實這是lambda表達式一個很要的功能,就是閉包。這裏咱們先講一下lambda表達式的大體原理:每當你定義一個lambda表達式後,編譯器會自動生成一個匿名類(這個類固然重載了()運算符),咱們稱爲閉包類型(closure type)。那麼在運行時,這個lambda表達式就會返回一個匿名的閉包實例,其實一個右值。因此,咱們上面的lambda表達式的結果就是一個個閉包。閉包的一個強大之處是其能夠經過傳值或者引用的方式捕捉其封裝做用域內的變量,前面的方括號就是用來定義捕捉模式以及變量,咱們又將其稱爲lambda捕捉塊。看下面的例子:數組
int main() { int x = 10; auto add_x = [x](int a) { return a + x; }; // 複製捕捉x auto multiply_x = [&x](int a) { return a * x; }; // 引用捕捉x cout << add_x(10) << " " << multiply_x(10) << endl; // 輸出:20 100 return 0; }
當lambda捕捉塊爲空時,表示沒有捕捉任何變量。可是上面的add_x是以複製的形式捕捉變量x,而multiply是以引用的方式捕捉x。前面講過,lambda表達式是產生一個閉包類,那麼捕捉是回事?對於複製傳值捕捉方式,類中會相應添加對應類型的非靜態數據成員。在運行時,會用複製的值初始化這些成員變量,從而生成閉包。前面說過,閉包類也實現了函數調用運算符的重載,通常狀況是:安全
class ClosureType { public: // ... ReturnType operator(params) const { body }; }
這意味着lambda表達式沒法修改經過複製形式捕捉的變量,由於函數調用運算符的重載方法是const屬性的。有時候,你想改動傳值方式捕獲的值,那麼就要使用mutable,例子以下:閉包
int main() { int x = 10; auto add_x = [x](int a) mutable { x *= 2; return a + x; }; // 複製捕捉x cout << add_x(10) << endl; // 輸出 30 return 0; }
這是爲何呢?由於你一旦將lambda表達式標記爲mutable,那麼實現的了函數調用運算符是非const屬性的:app
class ClosureType { public: // ... ReturnType operator(params) { body }; }
對於引用捕獲方式,不管是否標記mutable,均可以在lambda表達式中修改捕獲的值。至於閉包類中是否有對應成員,C++標準中給出的答案是:不清楚的,看來與具體實現有關。既然說到了深處,還有一點要注意:lambda表達式是不能被賦值的:less
auto a = [] { cout << "A" << endl; }; auto b = [] { cout << "B" << endl; }; a = b; // 非法,lambda沒法賦值 auto c = a; // 合法,生成一個副本
你可能會想a與b對應的函數類型是一致的(編譯器也顯示是相同類型:lambda [] void () -> void),爲何不能相互賦值呢?由於禁用了賦值操做符:函數
ClosureType& operator=(const ClosureType&) = delete;
可是沒有禁用複製構造函數,因此你仍然能夠用一個lambda表達式去初始化另一個lambda表達式而產生副本。而且lambda表達式也能夠賦值給相對應的函數指針,這也使得你徹底能夠把lambda表達式當作對應函數類型的指針。
閒話少說,納入正題,捕獲的方式能夠是引用也能夠是複製,可是具體說來會有如下幾種狀況來捕獲其所在做用域中的變量:
[]:默認不捕獲任何變量;
[=]:默認以值捕獲全部變量;
[&]:默認以引用捕獲全部變量;
[x]:僅以值捕獲x,其它變量不捕獲;
[&x]:僅以引用捕獲x,其它變量不捕獲;
[=, &x]:默認以值捕獲全部變量,可是x是例外,經過引用捕獲;
[&, x]:默認以引用捕獲全部變量,可是x是例外,經過值捕獲;
[this]:經過引用捕獲當前對象(實際上是複製指針);
[*this]:經過傳值方式捕獲當前對象;
在上面的捕獲方式中,注意最好不要使用[=]和[&]默認捕獲全部變量。首先說默認引用捕獲全部變量,你有很大可能會出現懸掛引用(Dangling references),由於引用捕獲不會延長引用的變量的聲明週期:
std::function<int(int)> add_x(int x) { return [&](int a) { return x + a; }; }
由於參數x僅是一個臨時變量,函數調用後就被銷燬,可是返回的lambda表達式卻引用了該變量,但調用這個表達式時,引用的是一個垃圾值,因此會產生沒有意義的結果。你可能會想,能夠經過傳值的方式來解決上面的問題:
std::function<int(int)> add_x(int x) { return [=](int a) { return x + a; }; }
是的,使用默認傳值方式能夠避免懸掛引用問題。可是採用默認值捕獲全部變量仍然有風險,看下面的例子:
class Filter { public: Filter(int divisorVal): divisor{divisorVal} {} std::function<bool(int)> getFilter() { return [=](int value) {return value % divisor == 0; }; } private: int divisor; };
這個類中有一個成員方法,能夠返回一個lambda表達式,這個表達式使用了類的數據成員divisor。並且採用默認值方式捕捉全部變量。你可能認爲這個lambda表達式也捕捉了divisor的一份副本,可是實際上大錯特錯。問題出如今哪裏呢?由於數據成員divisor對lambda表達式並不可見,你能夠用下面的代碼驗證:
// 類的方法,下面沒法編譯,由於divisor並不在lambda捕捉的範圍 std::function<bool(int)> getFilter() { return [divisor](int value) {return value % divisor == 0; }; }
那麼原來的代碼爲何可以捕捉到呢?仔細想一想,原來每一個非靜態方法都有一個this指針變量,利用this指針,你能夠接近任何成員變量,因此lambda表達式實際上捕捉的是this指針的副本,因此原來的代碼等價於:
std::function<bool(int)> getFilter() { return [this](int value) {return value % this->divisor == 0; }; }
儘管仍是以值方式捕獲,可是捕獲的是指針,其實至關於以引用的方式捕獲了當前類對象,因此lambda表達式的閉包與一個類對象綁定在一塊兒了,這也很危險,由於你仍然有可能在類對象析構後使用這個lambda表達式,那麼相似「懸掛引用」的問題也會產生。因此,採用默認值捕捉全部變量仍然是不安全的,主要是因爲指針變量的複製,實際上仍是按引用傳值。
經過前面的例子,你還能夠看到lambda表達式能夠做爲返回值。咱們知道lambda表達式能夠賦值給對應類型的函數指針。可是使用函數指針貌似並非那麼方便。因此STL定義在<functional>頭文件提供了一個多態的函數對象封裝std::function,其相似於函數指針。它能夠綁定任何類函數對象,只要參數與返回類型相同。以下面的返回一個bool且接收兩個int的函數包裝器:
std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };
而lambda表達式一個更重要的應用是其能夠用於函數的參數,經過這種方式能夠實現回調函數。其實,最經常使用的是在STL算法中,好比你要統計一個數組中知足特定條件的元素數量,經過lambda表達式給出條件,傳遞給count_if函數:
int value = 3; vector<int> v {1, 3, 5, 2, 6, 10}; int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });
再好比你想生成斐波那契數列,而後保存在數組中,此時你可使用generate函數,並輔助lambda表達式:
vector<int> v(10); int a = 0; int b = 1; std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; }); // 此時v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
此外,lambda表達式還用於對象的排序準則:
class Person { public: Person(const string& first, const string& last): firstName{first}, lastName{last} {} Person() = default; string first() const { return firstName; } string last() const { return lastName; } private: string firstName; string lastName; }; int main() { vector<Person> vp; // ... 添加Person信息 // 按照姓名排序 std::sort(vp.begin(), vp.end(), [](const Person& p1, const Person& p2) { return p1.last() < p2.last() || (p1.last() == p2.last() && p1.first() < p2.first()); }); // ... return 0; }
總之,對於大部分STL算法,能夠很是靈活地搭配lambda表達式來實現想要的效果。
前面講完了lambda表達式的基本使用,最後給出lambda表達式的完整語法:
// 完整語法 [ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body } // 可選的簡化語法 [ capture-list ] ( params ) -> ret { body } [ capture-list ] ( params ) { body } [ capture-list ] { body }
第一個是完整的語法,後面3個是可選的語法。這意味着lambda表達式至關靈活,可是照樣有必定的限制,好比你使用了拖尾返回類型,那麼就不能省略參數列表,儘管其多是空的。針對完整的語法,咱們對各個部分作一個說明:
capture-list:捕捉列表,這個不用多說,前面已經講過,記住它不能省略;
params:參數列表,能夠省略(可是後面必須緊跟函數體);
mutable:可選,將lambda表達式標記爲mutable後,函數體就能夠修改傳值方式捕獲的變量;
constexpr:可選,C++17,能夠指定lambda表達式是一個常量函數;
exception:可選,指定lambda表達式能夠拋出的異常;
attribute:可選,指定lambda表達式的特性;
ret:可選,返回值類型;
body:函數執行體。
若是想了解更多,能夠參考 cppreference lambda。
在C++14中,lambda又獲得了加強,一個是泛型lambda表達式,一個是lambda能夠捕捉表達式。這裏咱們對這兩項新特色進行簡單介紹。
前面講過,lambda表達式能夠按複製或者引用捕獲在其做用域範圍內的變量。而有時候,咱們但願捕捉不在其做用域範圍內的變量,並且最重要的是咱們但願捕捉右值。因此C++14中引入了表達式捕捉,其容許用任何類型的表達式初始化捕捉的變量。看下面的例子:
// 利用表達式捕獲,能夠更靈活地處理做用域內的變量 int x = 4; auto y = [&r = x, x = x + 1] { r += 2; return x * x; }(); // 此時 x 更新爲6,y 爲25 // 直接用字面值初始化變量 auto z = [str = "string"]{ return str; }(); // 此時z是const char* 類型,存儲字符串 string
能夠看到捕捉表達式擴大了lambda表達式的捕捉能力,有時候你能夠用std::move初始化變量。這對不能複製只能移動的對象很重要,好比std::unique_ptr,由於其不支持複製操做,你沒法以值方式捕捉到它。可是利用lambda捕捉表達式,能夠經過移動來捕捉它:
auto myPi = std::make_unique<double>(3.1415); auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; }; cout << circle_area(1.0) << endl; // 3.1415
其實用表達式初始化捕捉變量,與使用auto聲明一個變量的機理是相似的。
從C++14開始,lambda表達式支持泛型:其參數可使用自動推斷類型的功能,而不須要顯示地聲明具體類型。這就如同函數模板同樣,參數要使用類型自動推斷功能,只須要將其類型指定爲auto,類型推斷規則與函數模板同樣。這裏給出一個簡單例子:
auto add = [](auto x, auto y) { return x + y; }; int x = add(2, 3); // 5 double y = add(2.5, 3.5); // 6.0
函數對象是一個普遍的概念,由於全部具備函數行爲的對象均可以稱爲函數對象。這是一個高級抽象,咱們不關心對象究竟是什麼,只要其具備函數行爲。所謂的函數行爲是指的是可使用()調用並傳遞參數:
function(arg1, arg2, ...); // 函數調用
這樣來講,lambda表達式也是一個函數對象。可是這裏咱們所講的是一種特殊的函數對象,這種函數對象其實是一個類的實例,只不過這個類實現了函數調用符():
class X { public: // 定義函數調用符 ReturnType operator()(params) const; // ... };
這樣,咱們可使用這個類的對象,並把它當作函數來使用:
X f; // ... f(arg1, arg2); // 等價於 f.operator()(arg1, arg2);
仍是例子說話,下面咱們定義一個打印一個整數的函數對象:
// T須要支持輸出流運算符 template <typename T> class Print { public: void operator()(T elem) const { cout << elem << ' ' ; } }; int main() { vector<int> v(10); int init = 0; std::generate(v.begin(), v.end(), [&init] { return init++; }); // 使用for_each輸出各個元素(送入一個Print實例) std::for_each(v.begin(), v.end(), Print<int>{}); // 利用lambda表達式:std::for_each(v.begin(), v.end(), [](int x){ cout << x << ' ';}); // 輸出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9 return 0; }
能夠看到Print<int>的實例能夠傳入std::for_each,其表現能夠像函數同樣,所以咱們稱這個實例爲函數對象。你們可能會想,for_each爲何能夠既接收lambda表達式,也能夠接收函數對象,其實STL算法是泛型實現的,其不關心接收的對象究竟是什麼類型,可是必需要支持函數調用運算:
// for_each的相似實現 namespace std { template <typename Iterator, typename Operation> Operation for_each(Iterator act, Iterator end, Operation op) { while (act != end) { op(*act); ++act; } return op; } }
泛型提供了高級抽象,不管是lambda表達式、函數對象,仍是函數指針,均可以傳入for_each算法中。
本質上,函數對象是類對象,這也使得函數對象相比普通函數有本身的獨特優點:
函數對象帶有狀態:函數對象相對於普通函數是「智能函數」,這就如同智能指針相較於傳統指針。由於函數對象除了提供函數調用符方法,還能夠擁有其餘方法和數據成員。因此函數對象有狀態。即便同一個類實例化的不一樣的函數對象其狀態也不相同,這是普通函數所沒法作到的。並且函數對象是能夠在運行時建立。
每一個函數對象有本身的類型:對於普通函數來講,只要簽名一致,其類型就是相同的。可是這並不適用於函數對象,由於函數對象的類型是其類的類型。這樣,函數對象有本身的類型,這意味着函數對象能夠用於模板參數,這對泛型編程有很大提高。
函數對象通常快於普通函數:由於函數對象通常用於模板參數,模板通常會在編譯時會作一些優化。
這裏咱們看一個能夠擁有狀態的函數對象,其用於生成連續序列:
class IntSequence { public: IntSequence(int initVal) : value{ initVal } {} int operator()() { return ++value; } private: int value; }; int main() { vector<int> v(10); std::generate(v.begin(), v.end(), IntSequence{ 0 }); /* lambda實現一樣效果 int init = 0; std::generate(v.begin(), v.end(), [&init] { return ++init; }); */ std::for_each(v.begin(), v.end(), [](int x) { cout << x << ' '; }); //輸出:1, 2, 3, 4, 5, 6, 7, 8, 9, 10 return 0; }
能夠看到,函數對象能夠擁有一個私有數據成員,每次調用時遞增,從而產生連續序列。一樣地,你能夠用lambda表達式實現相似的效果,可是必須採用引用捕捉方式。可是,函數對象能夠實現更復雜的功能,而用lambda表達式則須要複雜的引用捕捉。考慮一個能夠計算均值的函數對象:
class MeanValue { public: MeanValue(): num{0}, sum{0} {} void operator()(int e) { ++num; sum += num; } double value() { return static_cast<double>(sum) / static_cast<double>(num); } private: int num; int sum; }; int main() { vector<int> v{ 1, 3, 5, 7 }; MeanValue mv = std::for_each(v.begin(), v.end(), MeanValue{}); cout << mv.value() << endl; // output: 2.5 return 0; }
能夠看到MeanValue對象中保存了兩個私有變量num和sum分別記錄數量與總和,最後能夠經過二者計算出均值。lambda表達式也能夠利用引用捕捉實現相似功能,可是會有點繁瑣。這也算是函數對象獨特的優點。
頭文件<functional>中預約義了一些函數對象,如算術函數對象,比較函數對象,邏輯運算函數對象及按位函數對象,咱們能夠在須要時使用它們。好比less<>是STL排序算法中的默認比較函數對象,因此默認的排序結果是升序,可是若是你想降序排列,你可使用greater<>函數對象:
vector<int> v{3, 4, 2, 9, 5}; // 升序排序 std::sort(v.begin(), v.end()); // output: 2, 3, 4, 5, 9 // 降序排列 std::sort(v.begin(), v.end(), std::greater<int>{}); // output: 9, 5, 4, 3, 2
更多有關函數對象的信息你們能夠參考這裏。
從設計模式來講,函數適配器是一種特殊的函數對象,是將函數對象與其它函數對象,或者特定的值,或者特定的函數相互組合的產物。因爲組合特性,函數適配器能夠知足特定的需求,頭文件<functional>定義了幾種函數適配器:
std::bind(op, args...):將函數對象op的參數綁定到特定的值args
std::mem_fn(op):將類的成員函數轉化爲一個函數對象
std::not1(op), std::not2(op):一元取反器和二元取反器
綁定器std::bind是最經常使用的函數適配器,它能夠將函數對象的參數綁定至特定的值。對於沒有綁定的參數可使用std::placeholers::_1, std::placeholers::_2等標記。咱們從簡單的例子開始,好比你想獲得一個減去固定樹的函數對象:
auto minus10 = std::bind(std::minus<int>{}, std::placeholders::_1, 10); cout << minus10(20) << endl; // 輸出10
有時候你能夠利用綁定器從新排列參數的順序,下面的綁定器交換兩個參數的位置:
// 逆轉參數順序 auto vminus = std::bind(std::minus<int>{}, std::placeholders::_2, std::placeholders::_1); cout << vminus(20, 10) << endl; // 輸出-10
綁定器還能夠互相嵌套,從而實現函數對象的組合:
// 定義一個接收一個參數,而後將參數加10再乘以2的函數對象 auto plus10times2 = std::bind(std::multiplies<int>{}, std::bind(std::plus<int>{}, std::placeholders::_1, 10), 2); cout << plus10times2(4) << endl; // 輸出: 28 // 定義3次方函數對象 auto pow3 = std::bind(std::multiplies<int>{}, std::bind(std::multiplies<int>{}, std::placeholders::_1, std::placeholders::_1), std::placeholders::_1); cout << pow3(3) << endl; // 輸出:27
利用不一樣函數對象組合,函數適配器能夠調用全局函數,下面的例子是不區分大小寫來判斷一個字符串是否包含一個特定的子串:
// 大寫轉換函數 char myToupper(char c) { if (c >= 'a' && c <= 'z') return static_cast<char>(c - 'a' + 'A'); return c; } int main() { string s{ "Internationalization" }; string sub{ "Nation" }; auto pos = std::search(s.begin(), s.end(), sub.begin(), sub.end(), std::bind(std::equal_to<char>{}, std::bind(myToupper, std::placeholders::_1), std::bind(myToupper, std::placeholders::_2))); if (pos != s.end()) { cout << sub << " is part of " << s << endl; } // 輸出:Nation is part of Internationalization return 0; }
注意綁定器默認是以傳值方綁定參數,若是須要引用綁定值,那麼要使用std::ref和std::cref函數,分別表明普通引用和const引用綁定參數:
void f(int& n1, int& n2, const int& n3) { cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n'; ++n1; ++n2; // ++n3; //沒法編譯 } int main() { int n1 = 1, n2 = 2, n3 = 3; auto boundf = std::bind(f, n1, std::ref(n2), std::cref(n3)); n1 = 10; n2 = 11; n3 = 12; cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n'; boundf(); cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n'; // Before function : 10 11 12 // In function : 1 11 12 // After function : 10 12 12 return 0; }
能夠看到,n1是以默認方式綁定到函數f上,故僅是一個副本,不會影響原來的n1變量,可是n2是以引用綁定的,綁定到f的參數與原來的n2相互影響,n3是以const引用綁定的,函數f沒法修改其值。
綁定器能夠用於調用類中的成員函數:
class Person { public: Person(const string& n) : name{ n } {} void print() const { cout << name << endl; } void print2(const string& prefix) { cout << prefix << name << endl; } private: string name; }; int main() { vector<Person> p{ Person{"Tick"}, Person{"Trick"} }; // 調用成員函數print std::for_each(p.begin(), p.end(), std::bind(&Person::print, std::placeholders::_1)); // 此處的std::placeholders::_1表示要調用的Person對象,因此至關於調用arg1.print() // 輸出:Tick Trick std::for_each(p.begin(), p.end(), std::bind(&Person::print2, std::placeholders::_1, "Person: ")); // 此處的std::placeholders::_1表示要調用的Person對象,因此至關於調用arg1.print2("Person: ") // 輸出:Person: Tick Person: Trick return 0; }
並且綁定器對虛函數也有效,你能夠本身作一下測試。
前面說過,C++11中lambda表達式沒法實現移動捕捉變量,可是使用綁定器能夠實現相似的功能:
vector<int> data{ 1, 2, 3, 4 }; auto func = std::bind([](const vector<int>& data) { cout << data.size() << endl; }, std::move(data)); func(); // 4 cout << data.size() << endl; // 0
能夠看到綁定器能夠實現移動語義,這是由於對於左值參數,綁定對象是複製構造的,可是對右值參數,綁定對象是移動構造的。
當想調用成員函數時,你還可使用std::mem_fn函數,此時你能夠省略掉用於調用對象的佔位符:
vector<Person> p{ Person{ "Tick" }, Person{ "Trick" } }; std::for_each(p.begin(), p.end(), std::mem_fn(&Person::print)); // 輸出: Trick Trick Person n{ "Bob" }; std::mem_fn(&Person::print2)(n, "Person: "); // 輸出:Person: Bob
因此,使用std::men_fn不須要綁定參數,能夠更方便地調用成員函數。再看一個例子,std;:mem_fn還能夠調用成員變量:
class Foo { public: int data = 7; void display_greeting() { cout << "Hello, world.\n"; } void display_number(int i) { cout << "number: " << i << '\n'; } }; int main() { Foo f; // 調用成員函數 std::mem_fn(&Foo::display_greeting)(f); // Hello, world. std::mem_fn(&Foo::display_number)(f, 20); // number: 20 // 調用數據成員 cout << std::mem_fn(&Foo::data)(f) << endl; // 7 return 0; }
取反器std::not1與std::not2很簡單,就是取函數對象的反結果,不過在C++17二者被棄用了,因此就不講了。