C++ lambda表達(dá)式與函數(shù)對(duì)象
lambda
表達(dá)式是C++11
中引入的一項(xiàng)新技術(shù),利用lambda
表達(dá)式可以編寫(xiě)內(nèi)嵌的匿名函數(shù),用以替換獨(dú)立函數(shù)或者函數(shù)對(duì)象,并且使代碼更可讀。但是從本質(zhì)上來(lái)講,lambda
表達(dá)式只是一種語(yǔ)法糖,因?yàn)樗衅淠芡瓿傻墓ぷ鞫伎梢杂闷渌晕?fù)雜的代碼來(lái)實(shí)現(xiàn)。但是它簡(jiǎn)便的語(yǔ)法卻給C++
帶來(lái)了深遠(yuǎn)的影響。如果從廣義上說(shuō),lamdba
表達(dá)式產(chǎn)生的是函數(shù)對(duì)象。在類(lèi)中,可以重載函數(shù)調(diào)用運(yùn)算符()
,此時(shí)類(lèi)的對(duì)象可以將具有類(lèi)似函數(shù)的行為,我們稱(chēng)這些對(duì)象為函數(shù)對(duì)象(Function Object)或者仿函數(shù)(Functor)。相比lambda
表達(dá)式,函數(shù)對(duì)象有自己獨(dú)特的優(yōu)勢(shì)。下面我們開(kāi)始具體講解這兩項(xiàng)黑科技。
lambda表達(dá)式
我們先從簡(jiǎn)答的例子開(kāi)始,我們定義一個(gè)可以輸出字符串的lambda
表達(dá)式,表達(dá)式一般都是從方括號(hào)[]
開(kāi)始,然后結(jié)束于花括號(hào){}
,花括號(hào)里面就像定義函數(shù)那樣,包含了lamdba
表達(dá)式體:
// 定義簡(jiǎn)單的lambda表達(dá)式
auto basicLambda = [] { cout << "Hello, world!" << endl; };
// 調(diào)用
basicLambda(); // 輸出:Hello, world!
上面是最簡(jiǎn)單的lambda
表達(dá)式,沒(méi)有參數(shù)。如果需要參數(shù),那么就要像函數(shù)那樣,放在圓括號(hào)里面,如果有返回值,返回類(lèi)型要放在->
后面,即拖尾返回類(lèi)型,當(dāng)然你也可以忽略返回類(lèi)型,lambda
會(huì)幫你自動(dòng)推斷出返回類(lèi)型:
// 指明返回類(lèi)型
auto add = [](int a, int b) -> int { return a + b; };
// 自動(dòng)推斷返回類(lèi)型
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è)匿名類(lèi)(這個(gè)類(lèi)當(dāng)然重載了()
運(yùn)算符),我們稱(chēng)為閉包類(lèi)型(closure type)。那么在運(yùn)行時(shí),這個(gè)lambda
表達(dá)式就會(huì)返回一個(gè)匿名的閉包實(shí)例,其實(shí)一個(gè)右值。所以,我們上面的lambda
表達(dá)式的結(jié)果就是一個(gè)個(gè)閉包。閉包的一個(gè)強(qiáng)大之處是其可以通過(guò)傳值或者引用的方式捕捉其封裝作用域內(nèi)的變量,前面的方括號(hào)就是用來(lái)定義捕捉模式以及變量,我們又將其稱(chēng)為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í),表示沒(méi)有捕捉任何變量。但是上面的add_x
是以復(fù)制的形式捕捉變量x
,而multiply
是以引用的方式捕捉x
。前面講過(guò),lambda
表達(dá)式是產(chǎn)生一個(gè)閉包類(lèi),那么捕捉是回事?對(duì)于復(fù)制傳值捕捉方式,類(lèi)中會(huì)相應(yīng)添加對(duì)應(yīng)類(lèi)型的非靜態(tài)數(shù)據(jù)成員。在運(yùn)行時(shí),會(huì)用復(fù)制的值初始化這些成員變量,從而生成閉包。前面說(shuō)過(guò),閉包類(lèi)也實(shí)現(xiàn)了函數(shù)調(diào)用運(yùn)算符的重載,一般情況是:
class ClosureType
{
public:
// ...
ReturnType operator(params) const { body };
}
這意味著lambda
表達(dá)式無(wú)法修改通過(guò)復(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ì)于引用捕獲方式,無(wú)論是否標(biāo)記mutable
,都可以在lambda
表達(dá)式中修改捕獲的值。至于閉包類(lèi)中是否有對(duì)應(yīng)成員,C++
標(biāo)準(zhǔn)中給出的答案是:不清楚的,看來(lái)與具體實(shí)現(xiàn)有關(guān)。既然說(shuō)到了深處,還有一點(diǎn)要注意:lambda
表達(dá)式是不能被賦值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda無(wú)法賦值
auto c = a; // 合法,生成一個(gè)副本
你可能會(huì)想a
與b
對(duì)應(yīng)的函數(shù)類(lèi)型是一致的(編譯器也顯示是相同類(lèi)型:lambda [] void () -> void),為什么不能相互賦值呢?因?yàn)榻昧速x值操作符:
ClosureType& operator=(const ClosureType&) = delete;
但是沒(méi)有禁用復(fù)制構(gòu)造函數(shù),所以你仍然可以用一個(gè)lambda
表達(dá)式去初始化另外一個(gè)lambda
表達(dá)式而產(chǎn)生副本。并且lambda
表達(dá)式也可以賦值給相對(duì)應(yīng)的函數(shù)指針,這也使得你完全可以把lambda
表達(dá)式看成對(duì)應(yīng)函數(shù)類(lèi)型的指針。
閑話少說(shuō),歸入正題,捕獲的方式可以是引用也可以是復(fù)制,但是具體說(shuō)來(lái)會(huì)有以下幾種情況來(lái)捕獲其所在作用域中的變量:
- []:默認(rèn)不捕獲任何變量;
- [=]:默認(rèn)以值捕獲所有變量;
- [&]:默認(rèn)以引用捕獲所有變量;
- [x]:僅以值捕獲x,其它變量不捕獲;
- [&x]:僅以引用捕獲x,其它變量不捕獲;
- [=, &x]:默認(rèn)以值捕獲所有變量,但是x是例外,通過(guò)引用捕獲;
- [&, x]:默認(rèn)以引用捕獲所有變量,但是x是例外,通過(guò)值捕獲;
- [this]:通過(guò)引用捕獲當(dāng)前對(duì)象(其實(shí)是復(fù)制指針);
- [*this]:通過(guò)傳值方式捕獲當(dāng)前對(duì)象;
在上面的捕獲方式中,注意最好不要使用[=]
和[&]
默認(rèn)捕獲所有變量。首先說(shuō)默認(rèn)引用捕獲所有變量,你有很大可能會(huì)出現(xiàn)懸掛引用(Dangling references),因?yàn)橐貌东@不會(huì)延長(zhǎng)引用的變量的聲明周期:
std::function<int(int)> add_x(int x)
{
return [&](int a) { return x + a; };
}
因?yàn)閰?shù)x
僅是一個(gè)臨時(shí)變量,函數(shù)調(diào)用后就被銷(xiāo)毀,但是返回的lambda
表達(dá)式卻引用了該變量,但調(diào)用這個(gè)表達(dá)式時(shí),引用的是一個(gè)垃圾值,所以會(huì)產(chǎn)生沒(méi)有意義的結(jié)果。你可能會(huì)想,可以通過(guò)傳值的方式來(lái)解決上面的問(wèn)題:
std::function<int(int)> add_x(int x)
{
return [=](int a) { return x + a; };
}
是的,使用默認(rèn)傳值方式可以避免懸掛引用問(wè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è)類(lèi)中有一個(gè)成員方法,可以返回一個(gè)lambda
表達(dá)式,這個(gè)表達(dá)式使用了類(lèi)的數(shù)據(jù)成員divisor
。而且采用默認(rèn)值方式捕捉所有變量。你可能認(rèn)為這個(gè)lambda
表達(dá)式也捕捉了divisor
的一份副本,但是實(shí)際上大錯(cuò)特錯(cuò)。問(wèn)題出現(xiàn)在哪里呢?因?yàn)閿?shù)據(jù)成員divisor
對(duì)lambda
表達(dá)式并不可見(jiàn),你可以用下面的代碼驗(yàn)證:
// 類(lèi)的方法,下面無(wú)法編譯,因?yàn)閐ivisor并不在lambda捕捉的范圍
std::function<bool(int)> getFilter()
{
return [divisor](int value) {return value % divisor == 0; };
}
那么原來(lái)的代碼為什么能夠捕捉到呢?仔細(xì)想想,原來(lái)每個(gè)非靜態(tài)方法都有一個(gè)this
指針變量,利用this
指針,你可以接近任何成員變量,所以lambda
表達(dá)式實(shí)際上捕捉的是this
指針的副本,所以原來(lái)的代碼等價(jià)于:
std::function<bool(int)> getFilter()
{
return [this](int value) {return value % this->divisor == 0; };
}
盡管還是以值方式捕獲,但是捕獲的是指針,其實(shí)相當(dāng)于以引用的方式捕獲了當(dāng)前類(lèi)對(duì)象,所以lambda
表達(dá)式的閉包與一個(gè)類(lèi)對(duì)象綁定在一起了,這也很危險(xiǎn),因?yàn)槟闳匀挥锌赡茉陬?lèi)對(duì)象析構(gòu)后使用這個(gè)lambda
表達(dá)式,那么類(lèi)似“懸掛引用”的問(wèn)題也會(huì)產(chǎn)生。所以,采用默認(rèn)值捕捉所有變量仍然是不安全的,主要是由于指針變量的復(fù)制,實(shí)際上還是按引用傳值。
通過(guò)前面的例子,你還可以看到lambda
表達(dá)式可以作為返回值。我們知道lambda
表達(dá)式可以賦值給對(duì)應(yīng)類(lèi)型的函數(shù)指針。但是使用函數(shù)指針貌似并不是那么方便。所以STL
定義在<functional>
頭文件提供了一個(gè)多態(tài)的函數(shù)對(duì)象封裝std::function
,其類(lèi)似于函數(shù)指針。它可以綁定任何類(lèi)函數(shù)對(duì)象,只要參數(shù)與返回類(lèi)型相同。如下面的返回一個(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ù),通過(guò)這種方式可以實(shí)現(xiàn)回調(diào)函數(shù)。其實(shí),最常用的是在STL
算法中,比如你要統(tǒng)計(jì)一個(gè)數(shù)組中滿足特定條件的元素?cái)?shù)量,通過(guò)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á)式來(lái)實(shí)現(xiàn)想要的效果。
前面講完了lambda
表達(dá)式的基本使用,最后給出lambda
表達(dá)式的完整語(yǔ)法:
// 完整語(yǔ)法
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }
// 可選的簡(jiǎn)化語(yǔ)法
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
第一個(gè)是完整的語(yǔ)法,后面3個(gè)是可選的語(yǔ)法。這意味著lambda
表達(dá)式相當(dāng)靈活,但是照樣有一定的限制,比如你使用了拖尾返回類(lèi)型,那么就不能省略參數(shù)列表,盡管其可能是空的。針對(duì)完整的語(yǔ)法,我們對(duì)各個(gè)部分做一個(gè)說(shuō)明:
- capture-list:捕捉列表,這個(gè)不用多說(shuō),前面已經(jīng)講過(guò),記住它不能省略;
- params:參數(shù)列表,可以省略(但是后面必須緊跟函數(shù)體);
-
mutable:可選,將
lambda
表達(dá)式標(biāo)記為mutable
后,函數(shù)體就可以修改傳值方式捕獲的變量; -
constexpr:可選,C++17,可以指定
lambda
表達(dá)式是一個(gè)常量函數(shù); -
exception:可選,指定
lambda
表達(dá)式可以拋出的異常; -
attribute:可選,指定
lambda
表達(dá)式的特性; - ret:可選,返回值類(lèi)型;
- body:函數(shù)執(zhí)行體。
如果想了解更多,可以參考 cppreference lambda。
lambda新特性
在C++14
中,lambda
又得到了增強(qiáng),一個(gè)是泛型lambda
表達(dá)式,一個(gè)是lambda
可以捕捉表達(dá)式。這里我們對(duì)這兩項(xiàng)新特點(diǎn)進(jìn)行簡(jiǎn)單介紹。
lambda捕捉表達(dá)式
前面講過(guò),lambda
表達(dá)式可以按復(fù)制或者引用捕獲在其作用域范圍內(nèi)的變量。而有時(shí)候,我們希望捕捉不在其作用域范圍內(nèi)的變量,而且最重要的是我們希望捕捉右值。所以C++14
中引入了表達(dá)式捕捉,其允許用任何類(lèi)型的表達(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* 類(lèi)型,存儲(chǔ)字符串 string
可以看到捕捉表達(dá)式擴(kuò)大了lambda
表達(dá)式的捕捉能力,有時(shí)候你可以用std::move
初始化變量。這對(duì)不能復(fù)制只能移動(dòng)的對(duì)象很重要,比如std::unique_ptr
,因?yàn)槠洳恢С謴?fù)制操作,你無(wú)法以值方式捕捉到它。但是利用lambda
捕捉表達(dá)式,可以通過(guò)移動(dòng)來(lái)捕捉它:
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ī)理是類(lèi)似的。
泛型lambda表達(dá)式
從C++14
開(kāi)始,lambda
表達(dá)式支持泛型:其參數(shù)可以使用自動(dòng)推斷類(lèi)型的功能,而不需要顯示地聲明具體類(lèi)型。這就如同函數(shù)模板一樣,參數(shù)要使用類(lèi)型自動(dòng)推斷功能,只需要將其類(lèi)型指定為auto
,類(lèi)型推斷規(guī)則與函數(shù)模板一樣。這里給出一個(gè)簡(jiǎn)單例子:
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ì)象都可以稱(chēng)為函數(shù)對(duì)象。這是一個(gè)高級(jí)抽象,我們不關(guān)心對(duì)象到底是什么,只要其具有函數(shù)行為。所謂的函數(shù)行為是指的是可以使用()
調(diào)用并傳遞參數(shù):
function(arg1, arg2, ...); // 函數(shù)調(diào)用
這樣來(lái)說(shuō),lambda
表達(dá)式也是一個(gè)函數(shù)對(duì)象。但是這里我們所講的是一種特殊的函數(shù)對(duì)象,這種函數(shù)對(duì)象實(shí)際上是一個(gè)類(lèi)的實(shí)例,只不過(guò)這個(gè)類(lèi)實(shí)現(xiàn)了函數(shù)調(diào)用符()
:
class X
{
public:
// 定義函數(shù)調(diào)用符
ReturnType operator()(params) const;
// ...
};
這樣,我們可以使用這個(gè)類(lèi)的對(duì)象,并把它當(dāng)做函數(shù)來(lái)使用:
X f;
// ...
f(arg1, arg2); // 等價(jià)于 f.operator()(arg1, arg2);
還是例子說(shuō)話,下面我們定義一個(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ù)一樣,因此我們稱(chēng)這個(gè)實(shí)例為函數(shù)對(duì)象。大家可能會(huì)想,for_each
為什么可以既接收lambda
表達(dá)式,也可以接收函數(shù)對(duì)象,其實(shí)STL
算法是泛型實(shí)現(xiàn)的,其不關(guān)心接收的對(duì)象到底是什么類(lèi)型,但是必須要支持函數(shù)調(diào)用運(yùn)算:
// for_each的類(lèi)似實(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ì)象是類(lèi)對(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è)類(lèi)實(shí)例化的不同的函數(shù)對(duì)象其狀態(tài)也不相同,這是普通函數(shù)所無(wú)法做到的。而且函數(shù)對(duì)象是可以在運(yùn)行時(shí)創(chuàng)建。
- 每個(gè)函數(shù)對(duì)象有自己的類(lèi)型:對(duì)于普通函數(shù)來(lái)說(shuō),只要簽名一致,其類(lèi)型就是相同的。但是這并不適用于函數(shù)對(duì)象,因?yàn)楹瘮?shù)對(duì)象的類(lèi)型是其類(lèi)的類(lèi)型。這樣,函數(shù)對(duì)象有自己的類(lèi)型,這意味著函數(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)類(lèi)似的效果,但是必須采用引用捕捉方式。但是,函數(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ù)量與總和,最后可以通過(guò)兩者計(jì)算出均值。lambda
表達(dá)式也可以利用引用捕捉實(shí)現(xiàn)類(lèi)似功能,但是會(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ì)模式來(lái)說(shuō),函數(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):將類(lèi)的成員函數(shù)轉(zhuǎn)化為一個(gè)函數(shù)對(duì)象
- std::not1(op), std::not2(op):一元取反器和二元取反器
綁定器(binder)
綁定器std::bind
是最常用的函數(shù)適配器,它可以將函數(shù)對(duì)象的參數(shù)綁定至特定的值。對(duì)于沒(méi)有綁定的參數(shù)可以使用std::placeholers::_1, std::placeholers::_2
等標(biāo)記。我們從簡(jiǎn)單的例子開(kāi)始,比如你想得到一個(gè)減去固定樹(shù)的函數(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ū)分大小寫(xiě)來(lái)判斷一個(gè)字符串是否包含一個(gè)特定的子串:
// 大寫(xiě)轉(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; //無(wú)法編譯
}
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ì)影響原來(lái)的n1
變量,但是n2
是以引用綁定的,綁定到f
的參數(shù)與原來(lái)的n2
相互影響,n3
是以const引用綁定的,函數(shù)f
無(wú)法修改其值。
綁定器可以用于調(diào)用類(lèi)中的成員函數(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è)試。
前面說(shuō)過(guò),C++11
中lambda
表達(dá)式無(wú)法實(shí)現(xiàn)移動(dòng)捕捉變量,但是使用綁定器可以實(shí)現(xiàn)類(lèi)似的功能:
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ǔ)義,這是因?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
很簡(jiǎn)單,就是取函數(shù)對(duì)象的反結(jié)果,不過(guò)在C++17
兩者被棄用了,所以就不講了。
References
[1] Marc Gregoire. Professional C++, Third Edition, 2016.
[2] cppreference
[3] 歐長(zhǎng)坤(歐龍崎), 高速上手 C++ 11/14.
[4] Scott Meyers. Effective Modern C++, 2014.
[5] Nicolai M. Josuttis. The C++ Standard Library
Second Edition, 2012.