目錄
一,lambda匿名函數
1-1,lambda的引入
1-2,lambda表達式書寫格式
1-3,lambda函數的名稱
1-4,lambda捕獲列表的使用
1-5,函數對象與lambda表達式
二,包裝器
2-1,function包裝器
2-2,bind包裝器
一,lambda匿名函數
1-1,lambda的引入
????????在C++中,lambda
函數是一種簡潔的匿名函數或表達式,能夠輕松處理復雜的邏輯和數據操作。lambda匿名函數可替換復雜的函數指針或偽函數,可以解決復雜而繁瑣的仿函數和函數指針的使用,以及讓程序員能夠將類似于函數的表達式用作接收函數指針或偽函數的函數的參數(這點與function的作用有關)。? ? ??
1-2,lambda表達式書寫格式
? ? ? ? lambda匿名函數:[capture-list] (parameters) mutable -> return-type { statement }
????????[capture-list]:捕捉列表,該列表總是出現在lambda函數的開始位置,編譯器根據[]來判斷接下來的代碼是否為lambda函數,捕捉列表能夠捕捉上下文中的變量(捕獲變量的值或引用‘&’)供lambda函數的函數體使用。
????????(parameters):參數列表,與普通函數的參數列表一致,如果不需要參數傳遞,則可以連同()一起省略。
????????mutable:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數為空)。lambda常性限制的是捕捉列表中的參數,沒有限制參數列表中的參數。
????????->returntype:lambda函數的返回值類型(注意:不是表達式返回類型,下面會詳細解釋這方面),沒有返回值時此部分可省略。由于lambda返回值類型相當于使用decltype根據返回值推斷得到,如果lambda不包含返回語句,推斷出的返回類型將為void,因此返回類型除非必要,一般不需要寫。若使用該指定返回類型,參數列表將不可省略(即使參數為空)。
????????{statement}:lambda函數的函數體,與普通函數的函數體一樣,包含實現功能的代碼。
注意:在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為 空。因此C++11中最簡單的lambda函數為:[]{}; 該lambda函數不能做任何事情。
#include <iostream>
#include <vector> ?
#include <algorithm>
using namespace std;
struct Goods
{
? ? string _name; ?// 名字
? ? double _price; // 價格
? ? int _evaluate; // 評價
? ? Goods(const char* str, double price, int evaluate)
? ? ? ? :_name(str)
? ? ? ? , _price(price)
? ? ? ? , _evaluate(evaluate)
? ? {}
};
struct ComparePriceLess //仿函數
{
? ? bool operator()(const Goods& gl, const Goods& gr)
? ? {
? ? ? ? return gl._price < gr._price;?
? ? }
};
struct ComparePriceGreater //仿函數
{
? ? bool operator()(const Goods& gl, const Goods& gr)
? ? {
? ? ? ? return gl._price > gr._price;
? ? }
};
int main()
{
? ? vector<Goods> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
? ?3 }, { "菠蘿", 1.5, 4 } };
? ? //下面兩個排序需要編寫兩個訪函數
? ? sort(v.begin(), v.end(), ComparePriceLess()); //按價格從小到大排序
? ? sort(v.begin(), v.end(), ComparePriceGreater()); //按價格從大到小排序
? ? //下面兩個排序使用lambda匿名函數,直接一個式子解決一種函數表達,可看出要比偽函數或函數指針(即直接函數實現)更簡便
? ? sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
? ? sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });
? ? /*上面[](const Goods& g1, const Goods& g2) {return g1._price < g2._price; }會自動推導函數提返回類型是bool,即與[](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; }等效*/
? ? return 0;
}
1-3,lambda函數的名稱
? ? ? ? 上面的lambda使用是作為單獨函數表達式,下面將lambda表達式用作名稱來使用(將該匿名函數賦給一個變量,該變量就是此名稱)。lambda函數表達式必須要使用auto類型來接收。lambda底層實際上是一個類的仿函數,它在C++中并不直接返回它的結果,而是創建了一個可調用的對象(一個函數對象)。因此,你不能直接將一個lambda
表達式賦值給一個非函數對象類型的變量(如int
)。編譯器會報錯,因為int
類型期望一個int
類型或與int
型相似類型的值,但是得到了一個lambda
表達式(一個類類型)。不僅如此,編譯器只有在編譯時才可以確定lambda生成的式子,不同編譯器下生成的式子還可能還不同,因此只能用auto接收lambda表達式,這里使用typeid().name可看出。
測試一:lambda的調用原理
//調用錯誤,lambda函數的返回值是int,但本身返回值不是int,是一個類類型
int a = [](int x)->int {return 55 + 5l; };
//auto自動推演類型,調用正確
auto _a = [](int x)->int {return 55 + 5l; };
//賦予lambda函數表達式名稱為_a,而lambda底層是類的訪函數,按照仿函數的調用即可
int b = _a(5);//上面lambda的調用相當于下面的類的仿函數調用
class A
{
public:
? ? int operator()(int x)
? ? {
? ? ? ? return 55 + 51;
? ? }
};A _a;
int b = _a(5);[](int x)->int {return 55 + 5l; };的調用相當于int x;A()(x);的調用
測試二:lambda原理調用的示例
#include <iostream>
using namespace std;int main()
{
? ? int x = 10;
? ? //使用lambda函數捕獲x的引用并修改它 ?
? ? auto m = [&x]() {x = x * x; }; //m是一個實現訪函數的類
? ? m(); //lambda函數的調用?
? ? cout << "x: " << x << endl; ?
? ? cout << typeid(m).name() << endl;?
? ? return 0;
}
????????lambda表達式是一個匿名函數,也可理解為一個表達式,該函數無法直接調用,如果想要直接調用,通常需要借助auto將其賦值給一個變量,通過該名稱變量調用。
1-4,lambda捕獲列表的使用
????????捕獲列表說明:捕捉列表用于傳入上下文中的數據(傳值或引用),以便供lambda使用。這里捕獲數據的方式有以下幾種:
????????[var]:表示值傳遞方式捕捉變量var
????????[&var]:表示引用傳遞捕捉變量var
????????[=]:表示值傳遞方式捕獲當前作用域中所有的變量(包括this)
????????[&]:表示引用傳遞捕獲當前作用域中所有的變量(包括this)
????????[this]:表示值傳遞方式捕捉當前的this指針
#include <iostream>
using namespace std;
int main()
{?? ?
?? ?int a = 10, b = 10;
?? ?//值傳遞捕捉a,捕捉的是a的拷貝
?? ?auto fun1 = [a, b]()mutable { a += 1; b += 1; return a + b; }; /*lambda默認是const函數,限制了捕捉列表的參數,這里要使用mutable取消對捕捉列表的限制*/
?? ?cout << "fun1: a + b = " << fun1() << endl;
?? ?cout << "a + b = " << a + b << endl;
?? ?cout << endl;
?? ?//引用傳遞捕捉b,捕捉的是b的引用
?? ?auto fun2 = [&a, &b] {a += 1; b += 1; return a + b; }; /*捕捉列表引用捕捉,說明開發者想要對其修改,lambda沒有對捕捉列表限制,可以修改*/
?? ?cout << "fun2: a + b = " << fun2() << endl;
?? ?cout << "a + b = " << a + b << endl;
?? ?cout << endl;
?? ?//值傳遞獲取當前作用域所有變量,捕捉所有變量的拷貝
?? ?int c = 10, d = 10;
?? ?auto fun3 = [=]()mutable {c += 1; d += 1; return c + d; }; //與上面“值傳遞捕捉a”同理
?? ?cout << "fun3: c + d = " << fun3() << endl;
?? ?cout << "c + d = " << c + d << endl;
?? ?cout << endl;
?? ?//引用傳遞獲取當前作用域所有變量,捕捉所有變量的引用
?? ?auto fun4 = [&] {c += 1; d += 1; return c + d; }; //與上面“引用傳遞捕捉b”同理
?? ?cout << "fun4: c + d = " << fun4() << endl;
?? ?cout << "c + d = " << c + d << endl;
?? ?cout << endl;
?? ?//捕捉a的拷貝,其它數據的引用
?? ?auto fun5 = [&, a]()mutable {a = 1; b = 1; c = 1; return a + b + c; };
?? ?/*auto fun6 = [=, &a]()mutable {a = 1; b = 1; c = 1; return a + b + c; };這里也可捕捉a的引用,其它數據的拷貝*/
?? ?cout << "fun5: a + b + c = " << fun5() << endl;
?? ?cout << "a + b + c = " << a + b + c;
?? ?cout << endl;
?? ?return 0;
}
? ? ? ? 這塊需說明以下幾個注意點:
? ? ? ? 1,語法上捕捉列表可由多個捕捉項組成,并以逗號分割,上面最后一個例子運用的就是此原理。還有比如:[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量。
? ? ? ? 2,捕捉列表不允許變量重復傳遞,否則就會導致編譯錯誤。比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重復。
????????4,在作用域中的lambda函數僅能捕捉當前作用域中局部變量,捕捉任何非此作用域或者
非局部變量都會導致編譯報錯。
? ? ? ? 5,lambda表達式之間不能相互賦值,即使看起來類型相同,因為無論怎樣lambda的類型都不可能一樣,它的底層是類的仿函數,編譯時確定的類型會有所不同。
? ? ? ? 6,lambda底層實現了拷貝構造,但是禁掉了默認構造。
#include <iostream>
using namespace std;
void (*PF)(); //函數指針的聲明
int main()
{
?? ?auto a = []{cout << "Hello C++" << endl; };
?? ?auto b = []{cout << "Hello C++" << endl; };?? ?/*a = b; 編譯失敗,lambda實現雖都一樣,但兩者的類型不一樣,內部不存在不同類型間的賦值。從下面的輸出可看出*/
?? ?cout << typeid(a).name() << endl;
?? ?cout << typeid(b).name() << endl;
?? ?cout << endl;
?? ?/*允許使用一個lambda表達式拷貝構造一個新的副本,兩者的類型相同,但lambda對象不能默認實現構造*/? ? //decltype(a) c; 默認構造的調用,編譯失敗
?? ?auto c(a);
?? ?cout << typeid(c).name() << endl;
?? ?cout << typeid(a).name() << endl;
?? ?cout << endl;
?? ?c();
?? ?//也可以將lambda表達式賦值給相同類型的函數指針
?? ?PF = a;
?? ?PF();
?? ?return 0;
}
1-5,函數對象與lambda表達式
? ? ? ? C++的函數對象通常指的是仿函數。lambda與函數對象極為相似,實際在底層編譯器對于lambda表達式的處理方式,完全就是按照函數對象的方式處理,即:如果定義了一個lambda表達式,編譯器會自動生成一個類,在該類中重載了operator(),即仿函數。下面的lambda表達式與函數對象基本可理解為一體。
class Rate
{
public:
?? ?Rate(double rate) : _rate(rate) {}
?? ?double operator()(double money, int year)
?? ?{
?? ??? ?return money * _rate * year;
?? ?}
private:
?? ?double _rate;
};
int main()
{
?? ?//函數對象
?? ?double rate = 0.49;
?? ?Rate r1(rate);
?? ?r1(10000, 2);
?? ?//lamber表達式
?? ?auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
?? ?r2(10000, 2);
?? ?return 0;
}
二,包裝器
2-1,function包裝器
????????function包裝器也叫作適配器。C++中的function本質是一個類模板,也是一個包裝器。由于函數指針較為復雜,仿函數比較單一,只能實現一種功能,如上Goods的比較,lambda函數語法層中沒有類型,auto接收的只是名稱,它們各有特色,但比較零散。function包裝器的主要作用是封裝函數指針(包括普通函數)、函數對象(仿函數)、lambda函數(匿名函數),將它們同一類型,實現一個函數的調用。我們先觀察下面代碼。
代碼一:
#include <iostream>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
?? ?static int count = 0;
?? ?cout << "count:" << ++count << endl;
?? ?cout << "count:" << &count << endl;
?? ?return f(x);
}
double f(double i)
{
?? ?return i / 2;
}
struct Functor
{
?? ?double operator()(double d)?
?? ?{
?? ??? ?return d / 3;
?? ?}
};
int main()
{
?? ?//函數名(函數指針)
?? ?cout << useF(f, 11.11) << endl;
?? ?cout << endl;
?? ?//函數對象(仿函數)
?? ?cout << useF(Functor(), 11.11) << endl;
?? ?cout << endl;
?? ?//lamber表達式
?? ?cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
?? ?cout << endl;
?? ?return 0;
}
代碼二:
#include <iostream>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
?? ?static int count = 0;
?? ?cout << "count:" << ++count << endl;
?? ?cout << "count:" << &count << endl;
?? ?return f(x);
}
int f1(double i) { return i / 2; }
double f2(double i) { return i / 3; }
double f3(double i) { return i / 4; }
double f4(int i) { return i / 5; }
int main()
{
?? ?/*實例化出的函數返回類型、參數類型相同,輸出的count相同,即同一個函數,但若是其中一個類型不同,將會實例化出不同的函數,即輸出count不同*/
?? ?cout << useF(f1, 11.11) << endl;
?? ?cout << useF(f2, 11.11) << endl;
?? ?cout << useF(f3, 11.11) << endl;
?? ?cout << useF(f4, 11.11) << endl;
?? ?return 0;
}
????????通過上面的程序驗證,我們會發現對于函數模板而言,當返回類型和參數類型一致的情況下,編譯器不會重新實例化出一份新的函數,但對于函數指針、函數對象、lambda表達式而言,即便三者的函數返回類型、形參類型都相同,但useF函數模板還是實例化了三份。包裝器function可以很好的解決上面的問題,將它們封裝成一種函數。
std::function在頭文件<functional>
類模板結構原型如下
????????template <class T> function;? ??
????????template <class Ret, class... Args>
????????class function<Ret(Args...)>;
模板參數說明:
????????Ret : 被調用函數的返回類型
????????Args…:被調用函數的形參類型
使用方法如下:
#include <iostream>
#include<functional>
using namespace std;template<class F, class T>
T useF(F f, T x)
{
?? ?static int count = 0;
?? ?cout << "count:" << ++count << endl;
?? ?cout << "count:" << &count << endl;
?? ?return f(x);
}double f1(double i) { return i / 2; }
int f2(double i) { return i / 2; }
double f3(int i) { return i / 3; }struct Functor
{
?? ?double operator()(double d) { return d / 3; }
};int main()
{
?? ?//函數指針
?? ?function<double(double)> fc1 = f1;
?? ?fc1(11.11); //將f1封裝成fc1,返回類型double、形參類型double
?? ?cout << useF(fc1, 11.11) << endl;
?? ?/*下面的函數指針形式輸出發現與上面的不同,返回類型和參數類型只要有一個不同,函數模板實例化出的函數就不同*/
?? ?function<double(int)> fcc1 = f2; ?
?? ?fcc1(11.11); //將f2封裝成fc,此時返回類型int、形參類型double,與f2不同
?? ?cout << useF(fcc1, 11.11) << endl;
?? ?//封裝函數返回類型和形參類型相同,調用函數相同
?? ?function<double(double)> fcc2 = f3;
?? ?fcc2(11.11); //將f3封裝成fc2,此時返回類型double、形參類型double,與f3不同
?? ?cout << useF(fcc2, 11.11) << endl;
?? ?cout << endl;?? ?//函數對象
?? ?function<double(double)> fc2 = Functor();
?? ?fc2(11.11);
?? ?cout << useF(fc2, 11.11) << endl;?? ?//lambda表達式
?? ?function<double(double)> fc3 = [](double d)->double { return d / 4; };
?? ?fc3(11.11);
?? ?cout << useF(fc3, 11.11) << endl;?? ?return 0;
}
? ? ? ? function包裝器封裝時的函數返回類型與形參類型可以與原函數不同,當包裝器包裝后,此時的調用情況與上面代碼二一樣,即若函數返回類型和形參類型相同將不會再新實例化出一份函數。
? ? ? ? function包裝器包裝后的調用與原函數互不影響,如上fc1與fc兩者調用的函數不同,這里可放心使用。
類的成員函數的包裝
? ? ? ? 類的成員函數分為靜態成員函數和非靜態成員函數。靜態成員函數沒有包含隱藏的this指針,函數名即為函數地址,包裝時跟其它函數包裝一樣,沒有任何變化。非靜態成員函數由于第一個參數是隱藏的this指針,所以語法規定包裝時第一個形參必須是對象的指針或對象,其次,類的成員函數名本身不是地址,所以這里傳遞時必須傳遞地址,即“&”。
#include <iostream>
#include<functional>
using namespace std;
class Plus
{
public:
?? ?static int plusi(int a, int b)
?? ?{
?? ??? ?return a + b;
?? ?}
?? ?double plusd(double a, double b)
?? ?{
?? ??? ?return a + b;
?? ?}
};int f(int a, int b) { return a + b; }
int main()
{
?? ?//普通函數
?? ?function<int(int, int)> fc1 = f;?
?? ?function<int(int, int)> f1 = &f;
?? ?cout << fc1(1, 1) << " ?" << f1(1, 1) << endl;
?? ?cout << f << " " << &f << endl; //輸出地址一樣,加不加&都行?? ?//靜態成員函數
?? ?function<int(int, int)> fc2 = &Plus::plusi;?
?? ?function<int(int, int)> fc = Plus::plusi;
?? ?cout << fc2(1, 1) << " ?" << fc(1, 1) << endl;
?? ?cout << &Plus::plusi << " " << Plus::plusi << endl; //輸出地址一樣,加不加&都行?? ?//非靜態成員函數
?? ?//非靜態成員函數需要對象的指針或者對象去進行調用,因為類的非靜態成員函數第一個默認隱形的參數是this指針
?? ?Plus plus;? ??//對象指針。這里不能使用Plus::plusd,因為成員函數名不是地址
?? ?function<double(Plus*, double, double)> fc3 = &Plus::plusd;?
?? ?cout << fc3(&plus, 1, 1) << endl; ?? ??//對象。這里不能使用Plus::plusd
?? ?function<double(Plus, double, double)> fc4 = &Plus::plusd;
?? ?cout << fc4(plus, 1, 1) << " ?" << fc4(Plus(), 1, 1) << endl;
?? ?return 0;
}
2-2,bind包裝器
? ? ? ? bind是一個函數模板,它就像一個函數包裝器(適配器),接受一個可調用對象,生成一個新的可調用對象來“適應”原對象的參數列表。它與function底層其實都是仿函數,返回的其實是一個可調用對象。
bind的作用有兩個:
????????1,調整可調用對象參數的順序(通常意義不大,了解即可)。
? ? ? ? 2,調整可調用對象參數的個數(具有一定的價值)。
bind原型結構如下
形式一:
????????template <class Fn, class... Args>
????????bind(Fn&& fn, Args&&... args);
形式二:(參數全部指定不能使用function接收,具體下面會說明)
????????template <class Ret, class Fn, class... Args>
????????bind(Fn&& fn, Args&&... args);
bind調用的一般形式:
1,auto萬能接收
auto newCallable = bind(callable, arg_list);?
2,function接收
function<Ret(Args...)>?newCallable =?bind(callable, arg_list);?
?????????newCallable:是一個可調用對象,可以是函數、成員函數、函數對象或lambda表達式。
? ? ? ? ?arg_list:是一個傳遞給callable,并以逗號分隔的形參列表。若是直接給定 callable 的參數,當我們調用newCallable時,newCallable會調用callable,并傳給指定的參數;若是 arg_list 中的參數存在占位符(placeholders占位符),則可以在稍后調用newCallable時提供這些參數。
? ? ? ? 占位符placeholders表示參數的“占位”。占位符_1、_2等表示綁定對象被調用時應提供的參數。
#include <iostream>
#include<functional>
using namespace std;int Sub(int a, int b)
{
?? ?return a - b;
}class Plus
{
public:
?? ?static int plusi(int a, int b)
?? ?{
?? ??? ?return a + b;
?? ?}?? ?double plusd(double a, double b)
?? ?{
?? ??? ?return a - b;
?? ?}
};int main()
{
?? ?//普通函數的正常參數順序使用
?? ?int x = 10, y = 20;
?? ?auto f1 = bind(Sub, 10, 20);
?? ?cout << f1() << endl; //指定參數,不需要傳遞?? ?auto f2 = bind(Sub, placeholders::_1, placeholders::_2); //成員_1表示傳遞Sub第一個位置參數,_2表示傳遞第二個位置參數
?? ?cout << f2(x, y) << endl; //占位符使用,x傳遞綁定對象f2的第一個參數,y傳遞第二個?? ?/*調整參數順序,f2第一個參數接收Sub的第二個位置上的參數,第二個參數接受Sub的第一個位置上的參數(此運用了解一下,意義不大)*/
?? ?function<int(int, int)> f3 = bind(Sub, placeholders::_2, placeholders::_1);?
?? ?cout << f3(x, y) << endl;?? ?//類的非靜態成員函數綁定
?? ?Plus p;
?? ?/*bind綁定Plus::plusd,此處要指名地址(成員函數名稱不是地址),具體綁定到p對象上,后面是綁定參數*/
?? ?function<double(double, double)> fc4 = bind(&Plus::plusd, p, placeholders::_1, placeholders::_2);
?? ?cout << fc4(2, 3) << endl;?? ?function<double(double)> fc5 = bind(&Plus::plusd, Plus(), placeholders::_1, 20); //綁定匿名對象Plus上
?? ?cout << fc5(2) << endl;? ??//類的靜態成員函數綁定
?? ?auto fc6?= bind(Plus::plusi, 10, 20); //靜態成員不屬于任何類,無需指名具體對象
//function<int(int, int)>fc6?= bind(Plus::plusi, 5, 7);參數全部指定,此時不能使用function
?? ?cout << fc6() << endl;
?? ?return 0;
}