C++ lambda表達(dá)式與函數(shù)對(duì)象
lambda
表達(dá)式是C++11
中引入的一項(xiàng)新技術(shù)夏志,利用lambda
表達(dá)式可以編寫內(nèi)嵌的匿名函數(shù)咐蚯,用以替換獨(dú)立函數(shù)或者函數(shù)對(duì)象童漩,并且使代碼更可讀。但是從本質(zhì)上來講春锋,lambda
表達(dá)式只是一種語法糖矫膨,因?yàn)樗衅淠芡瓿傻墓ぷ鞫伎梢杂闷渌晕?fù)雜的代碼來實(shí)現(xiàn)。但是它簡便的語法卻給C++
帶來了深遠(yuǎn)的影響期奔。如果從廣義上說侧馅,lamdba
表達(dá)式產(chǎn)生的是函數(shù)對(duì)象。在類中呐萌,可以重載函數(shù)調(diào)用運(yùn)算符()
馁痴,此時(shí)類的對(duì)象可以將具有類似函數(shù)的行為,我們稱這些對(duì)象為函數(shù)對(duì)象(Function Object)或者仿函數(shù)(Functor)肺孤。相比lambda
表達(dá)式罗晕,函數(shù)對(duì)象有自己獨(dú)特的優(yōu)勢(shì)。下面我們開始具體講解這兩項(xiàng)黑科技渠旁。
lambda表達(dá)式
我們先從簡答的例子開始攀例,我們定義一個(gè)可以輸出字符串的lambda
表達(dá)式,表達(dá)式一般都是從方括號(hào)[]
開始顾腊,然后結(jié)束于花括號(hào){}
粤铭,花括號(hào)里面就像定義函數(shù)那樣,包含了lamdba
表達(dá)式體:
// 定義簡單的lambda表達(dá)式
auto basicLambda = [] { cout << "Hello, world!" << endl; };
// 調(diào)用
basicLambda(); // 輸出:Hello, world!
上面是最簡單的lambda
表達(dá)式杂靶,沒有參數(shù)梆惯。如果需要參數(shù)酱鸭,那么就要像函數(shù)那樣,放在圓括號(hào)里面垛吗,如果有返回值凹髓,返回類型要放在->
后面,即拖尾返回類型怯屉,當(dāng)然你也可以忽略返回類型蔚舀,lambda
會(huì)幫你自動(dòng)推斷出返回類型:
// 指明返回類型
auto add = [](int a, int b) -> int { return a + b; };
// 自動(dòng)推斷返回類型
auto multiply = [](int a, int b) { return a * b; };
int sum = add(2, 5); // 輸出:7
int product = multiply(2, 5); // 輸出:10
大家可能會(huì)想lambda
表達(dá)式最前面的方括號(hào)的意義何在?其實(shí)這是lambda
表達(dá)式一個(gè)很要的功能锨络,就是閉包赌躺。這里我們先講一下lambda
表達(dá)式的大致原理:每當(dāng)你定義一個(gè)lambda
表達(dá)式后,編譯器會(huì)自動(dòng)生成一個(gè)匿名類(這個(gè)類當(dāng)然重載了()
運(yùn)算符)羡儿,我們稱為閉包類型(closure type)礼患。那么在運(yùn)行時(shí),這個(gè)lambda
表達(dá)式就會(huì)返回一個(gè)匿名的閉包實(shí)例掠归,其實(shí)一個(gè)右值缅叠。所以,我們上面的lambda
表達(dá)式的結(jié)果就是一個(gè)個(gè)閉包虏冻。閉包的一個(gè)強(qiáng)大之處是其可以通過傳值或者引用的方式捕捉其封裝作用域內(nèi)的變量肤粱,前面的方括號(hào)就是用來定義捕捉模式以及變量,我們又將其稱為lambda
捕捉塊厨相±欠福看下面的例子:
int main()
{
int x = 10;
auto add_x = [x](int a) { return a + x; }; // 復(fù)制捕捉x
auto multiply_x = [&x](int a) { return a * x; }; // 引用捕捉x
cout << add_x(10) << " " << multiply_x(10) << endl;
// 輸出:20 100
return 0;
}
當(dāng)lambda
捕捉塊為空時(shí),表示沒有捕捉任何變量领铐。但是上面的add_x
是以復(fù)制的形式捕捉變量x
,而multiply
是以引用的方式捕捉x
宋舷。前面講過绪撵,lambda
表達(dá)式是產(chǎn)生一個(gè)閉包類,那么捕捉是回事祝蝠?對(duì)于復(fù)制傳值捕捉方式音诈,類中會(huì)相應(yīng)添加對(duì)應(yīng)類型的非靜態(tài)數(shù)據(jù)成員。在運(yùn)行時(shí)绎狭,會(huì)用復(fù)制的值初始化這些成員變量细溅,從而生成閉包。前面說過儡嘶,閉包類也實(shí)現(xiàn)了函數(shù)調(diào)用運(yùn)算符的重載喇聊,一般情況是:
class ClosureType
{
public:
// ...
ReturnType operator(params) const { body };
}
這意味著lambda
表達(dá)式無法修改通過復(fù)制形式捕捉的變量,因?yàn)楹瘮?shù)調(diào)用運(yùn)算符的重載方法是const
屬性的蹦狂。有時(shí)候誓篱,你想改動(dòng)傳值方式捕獲的值朋贬,那么就要使用mutable
,例子如下:
int main()
{
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; }; // 復(fù)制捕捉x
cout << add_x(10) << endl; // 輸出 30
return 0;
}
這是為什么呢窜骄?因?yàn)槟阋坏?code>lambda表達(dá)式標(biāo)記為mutable
锦募,那么實(shí)現(xiàn)的了函數(shù)調(diào)用運(yùn)算符是非const屬性的:
class ClosureType
{
public:
// ...
ReturnType operator(params) { body };
}
對(duì)于引用捕獲方式,無論是否標(biāo)記mutable
邻遏,都可以在lambda
表達(dá)式中修改捕獲的值糠亩。至于閉包類中是否有對(duì)應(yīng)成員,C++
標(biāo)準(zhǔn)中給出的答案是:不清楚的准验,看來與具體實(shí)現(xiàn)有關(guān)赎线。既然說到了深處,還有一點(diǎn)要注意:lambda
表達(dá)式是不能被賦值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法沟娱,lambda無法賦值
auto c = a; // 合法氛驮,生成一個(gè)副本
你可能會(huì)想a
與b
對(duì)應(yīng)的函數(shù)類型是一致的(編譯器也顯示是相同類型:lambda [] void () -> void),為什么不能相互賦值呢济似?因?yàn)榻昧速x值操作符:
ClosureType& operator=(const ClosureType&) = delete;
但是沒有禁用復(fù)制構(gòu)造函數(shù)矫废,所以你仍然可以用一個(gè)lambda
表達(dá)式去初始化另外一個(gè)lambda
表達(dá)式而產(chǎn)生副本。并且lambda
表達(dá)式也可以賦值給相對(duì)應(yīng)的函數(shù)指針砰蠢,這也使得你完全可以把lambda
表達(dá)式看成對(duì)應(yīng)函數(shù)類型的指針蓖扑。
閑話少說,歸入正題台舱,捕獲的方式可以是引用也可以是復(fù)制律杠,但是具體說來會(huì)有以下幾種情況來捕獲其所在作用域中的變量:
- []:默認(rèn)不捕獲任何變量;
- [=]:默認(rèn)以值捕獲所有變量竞惋;
- [&]:默認(rèn)以引用捕獲所有變量柜去;
- [x]:僅以值捕獲x,其它變量不捕獲拆宛;
- [&x]:僅以引用捕獲x嗓奢,其它變量不捕獲;
- [=, &x]:默認(rèn)以值捕獲所有變量浑厚,但是x是例外股耽,通過引用捕獲;
- [&, x]:默認(rèn)以引用捕獲所有變量钳幅,但是x是例外物蝙,通過值捕獲;
- [this]:通過引用捕獲當(dāng)前對(duì)象(其實(shí)是復(fù)制指針)敢艰;
- [*this]:通過傳值方式捕獲當(dāng)前對(duì)象钙勃;
在上面的捕獲方式中裙顽,注意最好不要使用[=]
和[&]
默認(rèn)捕獲所有變量为黎。首先說默認(rèn)引用捕獲所有變量璃赡,你有很大可能會(huì)出現(xiàn)懸掛引用(Dangling references),因?yàn)橐貌东@不會(huì)延長引用的變量的聲明周期:
std::function<int(int)> add_x(int x)
{
return [&](int a) { return x + a; };
}
因?yàn)閰?shù)x
僅是一個(gè)臨時(shí)變量,函數(shù)調(diào)用后就被銷毀,但是返回的lambda
表達(dá)式卻引用了該變量,但調(diào)用這個(gè)表達(dá)式時(shí)柜砾,引用的是一個(gè)垃圾值,所以會(huì)產(chǎn)生沒有意義的結(jié)果换衬。你可能會(huì)想痰驱,可以通過傳值的方式來解決上面的問題:
std::function<int(int)> add_x(int x)
{
return [=](int a) { return x + a; };
}
是的,使用默認(rèn)傳值方式可以避免懸掛引用問題瞳浦。但是采用默認(rèn)值捕獲所有變量仍然有風(fēng)險(xiǎn)担映,看下面的例子:
class Filter
{
public:
Filter(int divisorVal):
divisor{divisorVal}
{}
std::function<bool(int)> getFilter()
{
return [=](int value) {return value % divisor == 0; };
}
private:
int divisor;
};
這個(gè)類中有一個(gè)成員方法,可以返回一個(gè)lambda
表達(dá)式叫潦,這個(gè)表達(dá)式使用了類的數(shù)據(jù)成員divisor
蝇完。而且采用默認(rèn)值方式捕捉所有變量。你可能認(rèn)為這個(gè)lambda
表達(dá)式也捕捉了divisor
的一份副本矗蕊,但是實(shí)際上大錯(cuò)特錯(cuò)短蜕。問題出現(xiàn)在哪里呢?因?yàn)閿?shù)據(jù)成員divisor
對(duì)lambda
表達(dá)式并不可見傻咖,你可以用下面的代碼驗(yàn)證:
// 類的方法朋魔,下面無法編譯,因?yàn)閐ivisor并不在lambda捕捉的范圍
std::function<bool(int)> getFilter()
{
return [divisor](int value) {return value % divisor == 0; };
}
那么原來的代碼為什么能夠捕捉到呢卿操?仔細(xì)想想警检,原來每個(gè)非靜態(tài)方法都有一個(gè)this
指針變量,利用this
指針害淤,你可以接近任何成員變量扇雕,所以lambda
表達(dá)式實(shí)際上捕捉的是this
指針的副本,所以原來的代碼等價(jià)于:
std::function<bool(int)> getFilter()
{
return [this](int value) {return value % this->divisor == 0; };
}
盡管還是以值方式捕獲窥摄,但是捕獲的是指針洼裤,其實(shí)相當(dāng)于以引用的方式捕獲了當(dāng)前類對(duì)象,所以lambda
表達(dá)式的閉包與一個(gè)類對(duì)象綁定在一起了溪王,這也很危險(xiǎn),因?yàn)槟闳匀挥锌赡茉陬悓?duì)象析構(gòu)后使用這個(gè)lambda
表達(dá)式值骇,那么類似“懸掛引用”的問題也會(huì)產(chǎn)生莹菱。所以,采用默認(rèn)值捕捉所有變量仍然是不安全的吱瘩,主要是由于指針變量的復(fù)制道伟,實(shí)際上還是按引用傳值。
通過前面的例子,你還可以看到lambda
表達(dá)式可以作為返回值蜜徽。我們知道lambda
表達(dá)式可以賦值給對(duì)應(yīng)類型的函數(shù)指針祝懂。但是使用函數(shù)指針貌似并不是那么方便。所以STL
定義在<functional>
頭文件提供了一個(gè)多態(tài)的函數(shù)對(duì)象封裝std::function
拘鞋,其類似于函數(shù)指針砚蓬。它可以綁定任何類函數(shù)對(duì)象,只要參數(shù)與返回類型相同盆色。如下面的返回一個(gè)bool且接收兩個(gè)int的函數(shù)包裝器:
std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };
而lambda
表達(dá)式一個(gè)更重要的應(yīng)用是其可以用于函數(shù)的參數(shù)灰蛙,通過這種方式可以實(shí)現(xiàn)回調(diào)函數(shù)。其實(shí)隔躲,最常用的是在STL
算法中摩梧,比如你要統(tǒng)計(jì)一個(gè)數(shù)組中滿足特定條件的元素?cái)?shù)量,通過lambda
表達(dá)式給出條件宣旱,傳遞給count_if
函數(shù):
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; });
再比如你想生成斐波那契數(shù)列仅父,然后保存在數(shù)組中,此時(shí)你可以使用generate
函數(shù)浑吟,并輔助lambda
表達(dá)式:
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; });
// 此時(shí)v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
此外笙纤,lambda
表達(dá)式還用于對(duì)象的排序準(zhǔn)則:
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;
}
總之,對(duì)于大部分STL
算法买置,可以非常靈活地搭配lambda
表達(dá)式來實(shí)現(xiàn)想要的效果粪糙。
前面講完了lambda
表達(dá)式的基本使用,最后給出lambda
表達(dá)式的完整語法:
// 完整語法
[ 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 }
第一個(gè)是完整的語法忿项,后面3個(gè)是可選的語法蓉冈。這意味著lambda
表達(dá)式相當(dāng)靈活,但是照樣有一定的限制轩触,比如你使用了拖尾返回類型寞酿,那么就不能省略參數(shù)列表,盡管其可能是空的脱柱。針對(duì)完整的語法伐弹,我們對(duì)各個(gè)部分做一個(gè)說明:
- capture-list:捕捉列表,這個(gè)不用多說榨为,前面已經(jīng)講過惨好,記住它不能省略;
- params:參數(shù)列表随闺,可以省略(但是后面必須緊跟函數(shù)體)日川;
-
mutable:可選,將
lambda
表達(dá)式標(biāo)記為mutable
后矩乐,函數(shù)體就可以修改傳值方式捕獲的變量龄句; -
constexpr:可選回论,C++17,可以指定
lambda
表達(dá)式是一個(gè)常量函數(shù)分歇; -
exception:可選傀蓉,指定
lambda
表達(dá)式可以拋出的異常; -
attribute:可選职抡,指定
lambda
表達(dá)式的特性葬燎; - ret:可選,返回值類型繁调;
- body:函數(shù)執(zhí)行體萨蚕。
如果想了解更多,可以參考 cppreference lambda蹄胰。
lambda新特性
在C++14
中岳遥,lambda
又得到了增強(qiáng),一個(gè)是泛型lambda
表達(dá)式裕寨,一個(gè)是lambda
可以捕捉表達(dá)式浩蓉。這里我們對(duì)這兩項(xiàng)新特點(diǎn)進(jìn)行簡單介紹。
lambda捕捉表達(dá)式
前面講過宾袜,lambda
表達(dá)式可以按復(fù)制或者引用捕獲在其作用域范圍內(nèi)的變量捻艳。而有時(shí)候,我們希望捕捉不在其作用域范圍內(nèi)的變量庆猫,而且最重要的是我們希望捕捉右值认轨。所以C++14
中引入了表達(dá)式捕捉,其允許用任何類型的表達(dá)式初始化捕捉的變量月培∴易郑看下面的例子:
// 利用表達(dá)式捕獲,可以更靈活地處理作用域內(nèi)的變量
int x = 4;
auto y = [&r = x, x = x + 1] { r += 2; return x * x; }();
// 此時(shí) x 更新為6杉畜,y 為25
// 直接用字面值初始化變量
auto z = [str = "string"]{ return str; }();
// 此時(shí)z是const char* 類型纪蜒,存儲(chǔ)字符串 string
可以看到捕捉表達(dá)式擴(kuò)大了lambda
表達(dá)式的捕捉能力,有時(shí)候你可以用std::move
初始化變量此叠。這對(duì)不能復(fù)制只能移動(dòng)的對(duì)象很重要纯续,比如std::unique_ptr
,因?yàn)槠洳恢С謴?fù)制操作灭袁,你無法以值方式捕捉到它猬错。但是利用lambda
捕捉表達(dá)式,可以通過移動(dòng)來捕捉它:
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
其實(shí)用表達(dá)式初始化捕捉變量茸歧,與使用auto
聲明一個(gè)變量的機(jī)理是類似的兔魂。
泛型lambda表達(dá)式
從C++14
開始,lambda
表達(dá)式支持泛型:其參數(shù)可以使用自動(dòng)推斷類型的功能举娩,而不需要顯示地聲明具體類型。這就如同函數(shù)模板一樣,參數(shù)要使用類型自動(dòng)推斷功能铜涉,只需要將其類型指定為auto
智玻,類型推斷規(guī)則與函數(shù)模板一樣。這里給出一個(gè)簡單例子:
auto add = [](auto x, auto y) { return x + y; };
int x = add(2, 3); // 5
double y = add(2.5, 3.5); // 6.0
函數(shù)對(duì)象
函數(shù)對(duì)象是一個(gè)廣泛的概念芙代,因?yàn)樗芯哂泻瘮?shù)行為的對(duì)象都可以稱為函數(shù)對(duì)象吊奢。這是一個(gè)高級(jí)抽象,我們不關(guān)心對(duì)象到底是什么纹烹,只要其具有函數(shù)行為页滚。所謂的函數(shù)行為是指的是可以使用()
調(diào)用并傳遞參數(shù):
function(arg1, arg2, ...); // 函數(shù)調(diào)用
這樣來說,lambda
表達(dá)式也是一個(gè)函數(shù)對(duì)象铺呵。但是這里我們所講的是一種特殊的函數(shù)對(duì)象裹驰,這種函數(shù)對(duì)象實(shí)際上是一個(gè)類的實(shí)例,只不過這個(gè)類實(shí)現(xiàn)了函數(shù)調(diào)用符()
:
class X
{
public:
// 定義函數(shù)調(diào)用符
ReturnType operator()(params) const;
// ...
};
這樣片挂,我們可以使用這個(gè)類的對(duì)象幻林,并把它當(dāng)做函數(shù)來使用:
X f;
// ...
f(arg1, arg2); // 等價(jià)于 f.operator()(arg1, arg2);
還是例子說話,下面我們定義一個(gè)打印一個(gè)整數(shù)的函數(shù)對(duì)象:
// T需要支持輸出流運(yùn)算符
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輸出各個(gè)元素(送入一個(gè)Print實(shí)例)
std::for_each(v.begin(), v.end(), Print<int>{});
// 利用lambda表達(dá)式: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>
的實(shí)例可以傳入std::for_each
音念,其表現(xiàn)可以像函數(shù)一樣沪饺,因此我們稱這個(gè)實(shí)例為函數(shù)對(duì)象。大家可能會(huì)想闷愤,for_each
為什么可以既接收lambda
表達(dá)式整葡,也可以接收函數(shù)對(duì)象,其實(shí)STL
算法是泛型實(shí)現(xiàn)的讥脐,其不關(guān)心接收的對(duì)象到底是什么類型遭居,但是必須要支持函數(shù)調(diào)用運(yùn)算:
// for_each的類似實(shí)現(xiàn)
namespace std
{
template <typename Iterator, typename Operation>
Operation for_each(Iterator act, Iterator end, Operation op)
{
while (act != end)
{
op(*act);
++act;
}
return op;
}
}
泛型提供了高級(jí)抽象,不論是lambda
表達(dá)式攘烛、函數(shù)對(duì)象魏滚,還是函數(shù)指針,都可以傳入for_each
算法中坟漱。
本質(zhì)上鼠次,函數(shù)對(duì)象是類對(duì)象,這也使得函數(shù)對(duì)象相比普通函數(shù)有自己的獨(dú)特優(yōu)勢(shì):
- 函數(shù)對(duì)象帶有狀態(tài):函數(shù)對(duì)象相對(duì)于普通函數(shù)是“智能函數(shù)”芋齿,這就如同智能指針相較于傳統(tǒng)指針腥寇。因?yàn)楹瘮?shù)對(duì)象除了提供函數(shù)調(diào)用符方法,還可以擁有其他方法和數(shù)據(jù)成員觅捆。所以函數(shù)對(duì)象有狀態(tài)赦役。即使同一個(gè)類實(shí)例化的不同的函數(shù)對(duì)象其狀態(tài)也不相同,這是普通函數(shù)所無法做到的栅炒。而且函數(shù)對(duì)象是可以在運(yùn)行時(shí)創(chuàng)建掂摔。
- 每個(gè)函數(shù)對(duì)象有自己的類型:對(duì)于普通函數(shù)來說术羔,只要簽名一致,其類型就是相同的乙漓。但是這并不適用于函數(shù)對(duì)象级历,因?yàn)楹瘮?shù)對(duì)象的類型是其類的類型。這樣叭披,函數(shù)對(duì)象有自己的類型寥殖,這意味著函數(shù)對(duì)象可以用于模板參數(shù),這對(duì)泛型編程有很大提升涩蜘。
- 函數(shù)對(duì)象一般快于普通函數(shù):因?yàn)楹瘮?shù)對(duì)象一般用于模板參數(shù)嚼贡,模板一般會(huì)在編譯時(shí)會(huì)做一些優(yōu)化。
這里我們看一個(gè)可以擁有狀態(tài)的函數(shù)對(duì)象同诫,其用于生成連續(xù)序列:
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實(shí)現(xiàn)同樣效果
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;
}
可以看到粤策,函數(shù)對(duì)象可以擁有一個(gè)私有數(shù)據(jù)成員,每次調(diào)用時(shí)遞增剩辟,從而產(chǎn)生連續(xù)序列掐场。同樣地,你可以用lambda
表達(dá)式實(shí)現(xiàn)類似的效果贩猎,但是必須采用引用捕捉方式熊户。但是,函數(shù)對(duì)象可以實(shí)現(xiàn)更復(fù)雜的功能吭服,而用lambda
表達(dá)式則需要復(fù)雜的引用捕捉嚷堡。考慮一個(gè)可以計(jì)算均值的函數(shù)對(duì)象:
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
對(duì)象中保存了兩個(gè)私有變量num
和sum
分別記錄數(shù)量與總和艇棕,最后可以通過兩者計(jì)算出均值蝌戒。lambda
表達(dá)式也可以利用引用捕捉實(shí)現(xiàn)類似功能,但是會(huì)有點(diǎn)繁瑣沼琉。這也算是函數(shù)對(duì)象獨(dú)特的優(yōu)勢(shì)北苟。
頭文件<functional>
中預(yù)定義了一些函數(shù)對(duì)象,如算術(shù)函數(shù)對(duì)象打瘪,比較函數(shù)對(duì)象友鼻,邏輯運(yùn)算函數(shù)對(duì)象及按位函數(shù)對(duì)象,我們可以在需要時(shí)使用它們闺骚。比如less<>
是STL
排序算法中的默認(rèn)比較函數(shù)對(duì)象彩扔,所以默認(rèn)的排序結(jié)果是升序,但是如果你想降序排列僻爽,你可以使用greater<>
函數(shù)對(duì)象:
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
更多有關(guān)函數(shù)對(duì)象的信息大家可以參考這里虫碉。
函數(shù)適配器
從設(shè)計(jì)模式來說,函數(shù)適配器是一種特殊的函數(shù)對(duì)象胸梆,是將函數(shù)對(duì)象與其它函數(shù)對(duì)象敦捧,或者特定的值须板,或者特定的函數(shù)相互組合的產(chǎn)物。由于組合特性兢卵,函數(shù)適配器可以滿足特定的需求逼纸,頭文件<functional>
定義了幾種函數(shù)適配器:
- std::bind(op, args...):將函數(shù)對(duì)象op的參數(shù)綁定到特定的值args
- std::mem_fn(op):將類的成員函數(shù)轉(zhuǎn)化為一個(gè)函數(shù)對(duì)象
- std::not1(op), std::not2(op):一元取反器和二元取反器
綁定器(binder)
綁定器std::bind
是最常用的函數(shù)適配器,它可以將函數(shù)對(duì)象的參數(shù)綁定至特定的值济蝉。對(duì)于沒有綁定的參數(shù)可以使用std::placeholers::_1, std::placeholers::_2
等標(biāo)記。我們從簡單的例子開始菠发,比如你想得到一個(gè)減去固定樹的函數(shù)對(duì)象:
auto minus10 = std::bind(std::minus<int>{}, std::placeholders::_1, 10);
cout << minus10(20) << endl; // 輸出10
有時(shí)候你可以利用綁定器重新排列參數(shù)的順序王滤,下面的綁定器交換兩個(gè)參數(shù)的位置:
// 逆轉(zhuǎn)參數(shù)順序
auto vminus = std::bind(std::minus<int>{}, std::placeholders::_2, std::placeholders::_1);
cout << vminus(20, 10) << endl; // 輸出-10
綁定器還可以互相嵌套,從而實(shí)現(xiàn)函數(shù)對(duì)象的組合:
// 定義一個(gè)接收一個(gè)參數(shù)滓鸠,然后將參數(shù)加10再乘以2的函數(shù)對(duì)象
auto plus10times2 = std::bind(std::multiplies<int>{},
std::bind(std::plus<int>{}, std::placeholders::_1, 10), 2);
cout << plus10times2(4) << endl; // 輸出: 28
// 定義3次方函數(shù)對(duì)象
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
利用不同函數(shù)對(duì)象組合雁乡,函數(shù)適配器可以調(diào)用全局函數(shù),下面的例子是不區(qū)分大小寫來判斷一個(gè)字符串是否包含一個(gè)特定的子串:
// 大寫轉(zhuǎn)換函數(shù)
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;
}
注意綁定器默認(rèn)是以傳值方綁定參數(shù)糜俗,如果需要引用綁定值踱稍,那么要使用std::ref
和std::cref
函數(shù),分別代表普通引用和const引用綁定參數(shù):
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
是以默認(rèn)方式綁定到函數(shù)f
上珠月,故僅是一個(gè)副本,不會(huì)影響原來的n1
變量楔敌,但是n2
是以引用綁定的啤挎,綁定到f
的參數(shù)與原來的n2
相互影響,n3
是以const引用綁定的卵凑,函數(shù)f
無法修改其值庆聘。
綁定器可以用于調(diào)用類中的成員函數(shù):
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"} };
// 調(diào)用成員函數(shù)print
std::for_each(p.begin(), p.end(), std::bind(&Person::print, std::placeholders::_1));
// 此處的std::placeholders::_1表示要調(diào)用的Person對(duì)象,所以相當(dāng)于調(diào)用arg1.print()
// 輸出:Tick Trick
std::for_each(p.begin(), p.end(), std::bind(&Person::print2, std::placeholders::_1,
"Person: "));
// 此處的std::placeholders::_1表示要調(diào)用的Person對(duì)象勺卢,所以相當(dāng)于調(diào)用arg1.print2("Person: ")
// 輸出:Person: Tick Person: Trick
return 0;
}
而且綁定器對(duì)虛函數(shù)也有效伙判,你可以自己做一下測(cè)試。
前面說過黑忱,C++11
中lambda
表達(dá)式無法實(shí)現(xiàn)移動(dòng)捕捉變量宴抚,但是使用綁定器可以實(shí)現(xiàn)類似的功能:
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
可以看到綁定器可以實(shí)現(xiàn)移動(dòng)語義,這是因?yàn)閷?duì)于左值參數(shù)杨何,綁定對(duì)象是復(fù)制構(gòu)造的酱塔,但是對(duì)右值參數(shù),綁定對(duì)象是移動(dòng)構(gòu)造的危虱。
std::mem_fn()適配器
當(dāng)想調(diào)用成員函數(shù)時(shí)羊娃,你還可以使用std::mem_fn
函數(shù),此時(shí)你可以省略掉用于調(diào)用對(duì)象的占位符:
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
不需要綁定參數(shù)蕊玷,可以更方便地調(diào)用成員函數(shù)邮利。再看一個(gè)例子,std;:mem_fn
還可以調(diào)用成員變量:
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;
// 調(diào)用成員函數(shù)
std::mem_fn(&Foo::display_greeting)(f); // Hello, world.
std::mem_fn(&Foo::display_number)(f, 20); // number: 20
// 調(diào)用數(shù)據(jù)成員
cout << std::mem_fn(&Foo::data)(f) << endl; // 7
return 0;
}
取反器std::not1
與std::not2
很簡單垃帅,就是取函數(shù)對(duì)象的反結(jié)果延届,不過在C++17
兩者被棄用了,所以就不講了贸诚。
References
[1] Marc Gregoire. Professional C++, Third Edition, 2016.
[2] cppreference
[3] 歐長坤(歐龍崎), 高速上手 C++ 11/14.
[4] Scott Meyers. Effective Modern C++, 2014.
[5] Nicolai M. Josuttis. The C++ Standard Library
Second Edition, 2012.