C++ lambda表達(dá)式與函數(shù)對(duì)象

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ì)想ab對(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è)私有變量numsum分別記錄數(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::refstd::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++11lambda表達(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::not1std::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.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末方庭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子酱固,更是在濱河造成了極大的恐慌械念,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件运悲,死亡現(xiàn)場(chǎng)離奇詭異龄减,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)班眯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門希停,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人署隘,你說我怎么就攤上這事宠能。” “怎么了定踱?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵棍潘,是天一觀的道長。 經(jīng)常有香客問我崖媚,道長亦歉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任畅哑,我火速辦了婚禮肴楷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荠呐。我一直安慰自己赛蔫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布泥张。 她就那樣靜靜地躺著呵恢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪媚创。 梳的紋絲不亂的頭發(fā)上渗钉,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼鳄橘。 笑死声离,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瘫怜。 我是一名探鬼主播术徊,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鲸湃!你這毒婦竟也來了赠涮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤暗挑,失蹤者是張志新(化名)和其女友劉穎世囊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窿祥,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年蝙寨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晒衩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡墙歪,死狀恐怖听系,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虹菲,我是刑警寧澤靠胜,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站毕源,受9級(jí)特大地震影響浪漠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜霎褐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一址愿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冻璃,春花似錦响谓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跋炕,卻和暖如春赖晶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背枣购。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工嬉探, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留擦耀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓涩堤,卻偏偏與公主長得像眷蜓,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子胎围,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容