一.lambda表達式
1.可調用對象
可調用對象即可以像函數一樣被調用的對象,有以下三種:
- 函數(指針)
- 仿函數對象
- lambda表達式
?tips:調用函數時,既可以用函數名,也可以用函數地址,因為函數名和函數地址是一回事。
2.lambda表達式格式
[捕捉列表](參數列表)mutable->返回值類型?{函數體}?
- 捕捉列表不能省略,即使它為空
- 參數列表為空時可以省略,但是有mutable時不能省略
- mutable用法后面會講
- 返回值類型可以省略
例如:<algorithm>中的sort是一個函數模版,第三個參數需要一個可調用對象,我們就可以傳一個lambda表達式過去。如果要傳仿函數對象,需要定義一個類,相比之下,lambda表達式更加輕便。
struct Good
{Good(const string& name, double price, int id):_name(name),_price(price),_id(id){}string _name;double _price;int _id;
};
int main()
{vector<Good> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2 ,3 }, { "菠蘿", 1.5, 4 } };auto priceLess = [](const Good& g1, const Good& g2)->bool{return g1._price < g2._price;};sort(v.begin(), v.end(), priceLess);
}
3.lambda表達式的本質
lambda表達式底層是一個仿函數對象,是一個重載了operator()的類的匿名對象。這個類的名稱是<lambda_uuid>(uuid是一個很長的字符串,通過某種算法得到,同一臺機器上基本不會重復),這個類對用戶也是匿名的,不能直接使用。
?4.詳解捕捉列表
捕捉列表用于捕捉父作用域的局部變量,供函數體中使用。父作用域指的是lambda表達式所在的語句塊。捕捉列表內的東西實質上是該匿名對象的成員。
(1)值捕捉
int main()
{int x = 0, y = 1;//捕捉lambda表達式函數體的父作用域,即main函數作用域內的變量x和變量yauto f = [x, y]()mutable->void{int tmp = x;x = y;y = tmp;};f();cout << "x = " << x << endl;cout << "y = " << y << endl;return 0;
}
lambda表達式是一個仿函數對象,它的operator()函數是const的,也即函數體內不能修改成員,使用mutalbe可以取消const。lambda表達式不建議取消const特性。
可以看出x和y的值并沒有交換,因為捕捉列表中的x和y是main函數中的x,y的拷貝,二者互不影響。
(2)引用捕捉
int main()
{int x = 0, y = 1;auto f = [&x, &y]()->void //不用加mutable,因為引用可以修改{int tmp = x;x = y;y = tmp;};f();cout << "x = " << x << endl;cout << "y = " << y << endl;return 0;
}
(3)全部傳值捕捉
int main()
{int x = 0, y = 1;auto f = [=]()->void{cout << x << endl;cout << y << endl;};f();return 0;
}
如果lambda表達式在成員函數中,那么this指針也會捕捉過來?
class AA
{
public:void func(){auto f1 = [=]()->void{cout << _a;//this->_acout << _b;//this->_b};//這樣寫反而不行,因為_a和_b不在func作用域內/*auto f1 = [_a, _b]()->void{cout << _a;cout << _b;};*/}
private:int _a;int _b;
};
(4)全部引用捕捉
int main()
{int x = 0, y = 1;auto f = [&]()->void{cout << x << endl;cout << y << endl;};f();return 0;
}
同理,如果lambda表達式在成員函數中,this指針也會捕捉過來。?
?(5)混合捕捉
int main()
{int x = 0, y = 1;auto f1 = [&, x]()->void //傳值捕捉x,其余全部引用捕捉,&或=必須在前{cout << x << endl;cout << y << endl;};auto f2 = [x, &y]()->void //傳值捕捉x,引用捕捉y{cout << x << endl;cout << y << endl;};//auto f3 = [=, &]()->void //不允許重復捕捉,因為使用時會有歧義//{// cout << x << endl;// cout << y << endl;//};return 0;
}
前文說到,捕捉列表中的東西實質上是匿名對象的成員,證明如下:
由結果可知,當捕捉父作用域的全部局部變量時,編譯器并非憨憨地全部捕捉,而是看你在函數體內用到了哪些,沒用到的就不捕獲了。?
二.function包裝器
1.function包裝器用法
- function是一個類模版,在<functional>中。
- 不同的可調用對象雖然類型不同,但是卻可能有相同的調用形式,即返回類型,參數類型相同。
- function包裝器作用就是包裝可調用對象,把調用形式相同的可調用對象的類型統一起來,便于書寫它們的類型。
- function實際是一個類模版,實例化時的模版參數寫可調用對象的調用形式,即返回值類型(參數類型列表),如void(int, double),這一點和普通模版不一樣。
void swap_func(int& x, int& y)
{int tmp = x;x = y;y = x;cout << "函數指針" << endl;
}class Swap_func
{
public:void operator()(int& x, int& y){int tmp = x;x = y;y = x;cout << "仿函數對象" << endl;}
};
int main()
{int x = 1, y = 0;function<void(int&, int&)> f1 = swap_func;f1(x, y);auto lambda_swap_func = [](int& x, int& y)->void{int tmp = x;x = y;y = x;cout << "lambda表達式" << endl;};f1 = lambda_swap_func;f1(x, y);f1 = Swap_func();f1(x, y);return 0;
}
?
?將具有同種調用形式的可調用對象類型統一起來,下面這段代碼能體現其用處:
map<string, function<void(int&, int&)>> cmdOP = {{"函數指針", swap_func},{"lambda表達式", lambda_swap_func},{"仿函數對象", Swap_func()}};//列表初始化cmdOP["函數指針"](x, y);cmdOP["lambda表達式"](x, y);cmdOP["仿函數對象"](x, y);
如果沒有function包裝器,map的第二個模版參數就只能用函數指針了,并且只能使用函數這一種可調用對象,缺乏靈活性。并且函數指針用起來很麻煩,這也是為什么C++仿函數被廣泛應用的原因。
2.function包裝器包裝成員函數
(1)靜態成員函數
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
int main()
{//包裝靜態成員函數function<int(int, int)> f1 = Plus::plusi;int ret = f1(1, 2);return 0;
}
與普通全局函數唯一不同的一點,就是要指明類域。
(2)非靜態成員函數
function<double(double, double)> f2 = Plus::plusd;
//error C3867: “Plus::plusd”: 非標準語法;請使用 "&" 來創建指向成員的指針
非靜態成員函數比較特殊,函數前還需加上取地址符號,一般情況下不管函數取不取地址,都代表函數地址,這里特殊情況記住就行。
function<double(double, double)> f2 = &Plus::plusd;
//error C2440: “初始化”: 無法從“double (__thiscall Plus::* )(double,double)”
//轉換為“std::function<double (double,double)>”
還是有問題,因為成員函數都是隱式傳了this指針的,不然函數體內就無法訪問成員變量。
最終寫法:
int main()
{double x = 1.1;double y = 2.2;function<double(Plus*, double, double)> f2 = &Plus::plusd;double ret = f2(&p, x, y);return 0;
}
還支持這樣寫:
function<double(Plus, double, double)> f2 = &Plus::plusd;
f2(Plus(), x, y);//傳匿名對象
在底層上可以理解為前者是通過對象指針去調用plusd,后者通過對象調用plusd,二者是一樣的。而且后者可以傳匿名對象,更加方便。
但是,每次調用f2都好麻煩啊,需要傳遞對象或者對象指針。如果函數體內部根本不涉及成員變量的操作,也就是說第一個參數傳什么根本不重要。有沒有什么辦法,每次自動傳一個匿名對象,讓我自己少傳一個參數呢?有的,使用bind函數!!!
三.bind包裝器
- bind是一個函數模版,在<functional>中
- 它的作用是包裝可調用對象,返回一個新的可調用對象,以達到調整調用形式的目的,即參數個數及順序
一般形式:auto newCallable = bind(callable, arg_list)??
callalbe是原來的可調用對象,newCallable是返回的新的可調用對象,arg_list是以逗號分隔的參數列表
?
1.改變參數順序
int Sub(int x, int y)
{return x - y;
}
int main()
{//調整參數順序auto f1 = bind(Sub, placeholders::_2, placeholders::_1);cout << f1(10, 5) << endl;return 0;
}
其中_n(n=1, 2, 3……)是“占位符”,表示新的可調用對象的參數,它定義在命名空間placeholders中。實際上,bind返回的newCallable封裝了Callable,當我們調用newCallable時,newCallable會調用Callable,并傳遞給它arg_list中的參數。
2.改變參數個數
bind意為“綁定”,它真正的作用在于綁定某些參數,從而減少傳參的個數。
int main()
{//調整參數個數auto lambda = [](int a, int b, int c)->void{cout << a << endl;cout << b << endl;cout << c << endl;};function<void(int, int)> f = bind(lambda, placeholders::_1, 10, placeholders::_2);f(1, 100);
}
?
注意觀察,bind返回的是一個可調用對象,當然可以用function包裝。并且function的模版參數就是新的可調用對象的調用形式。?
同理,用function包裝非靜態成員函數時就可以使用bind來減少參數傳遞?
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};int main()
{function<double(double, double)> f = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);double ret = f(1.1, 2.2);cout << ret << endl;
}
3.bind返回的可調用對象本質
可見,bind返回的可調用對象也是一個類的實例對象,這個類肯定封裝了operator()。其實無論是lambda表達式,還是bind返回的對可調用象,都是用的仿函數技術,這不過外面做了一層精美的包裝,使我們使用起來更加方便。