杂货边角(11):C++11之lambda匿名函数

匿名函数lambda思想是函数式编程的基础,其中以lisp为代表,以其抽象和迭代的思想让无数coder奉为圣经。函数式编程、命令式编程、面向对象编程、元编程等都是一种编程范型。而近年来的趋势是高级语言越来越多地引入多范型编程支持(如C++和Python都开始支持函数式编程)。C++11引入lambda特性,对于泛型编程和函数式编程的支持都极大提升。相比于lisp的lambda函数等同于C++98中的匿名局部函数,C++11中的lambda匿名函数更接近lisp中的闭包closure概念。
目录:

Sec1. C++11中的lambda函数语法

Lambda函数的语法定义如下:
[capture](parameters) mutable -> return-type{statement}
其中
[capture]:捕获列表,捕捉列表总是出现在lambda函数的开始处,事实上,[ ]lambda引出符,编译器根据该引出符判断接下来的代码是否是lambda函数。捕获列表能够捕捉上下文中的变量以供lambda函数使用:
1.[var]表示值传递方式捕获变量var
2.[=] 表示值传递方式捕获所有父作用域的变量(包括this)
3.[&var] 表示引用传递捕获变量var
4.[&]表示引用传递捕获所有父作用域的变量(包括this)
5.[this] 表示值传递方式捕获当前的this指针
6.[=, &a, &b] 表示以引用传递的方式捕获变量a和b, 值传递方式捕获其他所有变量
7.[&, a, this]表示以值传递的方式捕获变量a和this, 引用传递方式捕获其他所有变量
捕获列表也正是C++11中的lambda具有闭包属性的来源,传统的局部匿名函数只是lambda的一种捕获列表[ ]为空的特例。

(parameters):参数列表,和普通函数的参数列表一样,如果不需要传递参数,则可以连同括号( )一起省略;optional

mutable: mutable修饰符,默认情况下,lambda函数总是一个const函数(即不修改已有环境),mutable可以取消其常量属性,在使用mutable修饰符时,参数列表不可省略,即使参数为空;optional。mutable更多的是提供一种语法上的自由度,实际编程中很少用到该关键词,因为既然是“闭包”,那么在复制了上下文环境变量后,就应该关闭对外界环境的影响。

->return-type: 返回类型,用追踪返回类型形式声明函数的返回类型,出于方便,不需要返回值的时候也可以连同符号->一起省略,此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行自动推导。optional

{statement}: 函数体,内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

C++里的lambda匿名函数,某种程度上等同于闭包函数,因为lambda函数不仅可以通过参数列表输入参数,还可以通过捕获列表显式地直接捕获上下文环境中的数据,并可以指定是值传递还是引用传递的方式。

Sec2. lambda参数值传递和引用传递的区别以及mutable的使用

#include <iostream>

using namespace std;
int main() {
    int j = 12;
    auto by_val_lambda = [=] {return j+1; };
    auto by_ref_lambda = [&] {return j+1; };
    cout<< "by_val_lambda: " << by_val_lambda() << endl;
    cout<< "by_ref_lambda: " << by_ref_lambda() << endl;

    j++;
    cout<< "by_val_lambda: " << by_val_lambda() << endl;
    cout<< "by_ref_lambda: " << by_ref_lambda() << endl;

    return 0;

}

这里写图片描述

摘录《深入理解C++11》书中的内容:“简单地总结来说,在使用lambda函数的时候,如果需要捕捉的值成为lambda函数的常量,我们通常会使用按值传递的方式捕捉;反之,需要捕捉的值成为lambda函数运行时的变量(类似于参数的效果),则应该采用按引用方式进行捕捉”。其实,我觉得还是因为在采用值传递的语义下,匿名lambda函数认为这些为常量,第一次初始化后将不再发生变化。

关于lambda使用mutable关键字用来修改常量性,可以通过以下的例子直观感受一下。

#include <iostream>

using namespace std;
int main() {
    int val = 0;
    //声明父域变量值传递,这些变量在lambda为常量性,不可更改
    //auto const_val_lambda = [=]() {val = 3; };//error: assignment of read-only variable 'val'

    //声明父域变量值传递,但是这些变量值在lambda内部并非const
    auto mutable_val_lambda = [=] () mutable {val = 3; };
    mutable_val_lambda();
    cout<<"val: "<<val<<endl;
    //声明父域变量引用传递
    auto const_ref_lambda = [&] {val = 13; };
    const_ref_lambda();
    cout<<"val: "<<val<<endl;
    //通过参数列表传进去
    auto const_param_lambda = [](int v) {v = 23;};
    const_param_lambda(val);
    cout<<"val: "<<val<<endl;

    return 0;
}

这里写图片描述

Sec3. lambda和仿函数的区别

好的编程语言一般都有好的库支持,不然Python也不会再近年来蹿升地这么快,C++也不例外。C++语言在标准程序库STL中向用户提供了一些基本的数据结构及一些基本的算法等。在C++11之前,我们在使用STL算法时,通常会使用一种特别的对象,一般来说,我们称之为函数对象,或者仿函数。仿函数简单地说,就是重定义了class类的成员函数operator()的一种自定义类型对象,这样的对象有个特点,就是其使用在代码层面感觉跟函数的使用并无二样,但究其本质却并非函数。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

/*
仿函数的设计,使用起来像是函数,但其实是类的对象,只不过该类重载operator()成员函数
operator()函数调用运算符,重载()符号
*/
class _functor {
public:
    int operator() (int x, int y) {return x+y;}
};

class Tax {
private:
    float rate;
    int base;
public:
    Tax(float r, int b):rate(r), base(b) {}
    float operator() (float money) {return (money - base)* rate;}
};

class AirportPrice {
private:
    float _dutyfreerate;
public:
    AirportPrice(float rate): _dutyfreerate(rate) {}
    float operator() (float price) { return price*(1-_dutyfreerate/100); }
};

int main()
{
    int girls = 3, boys = 4;
    _functor totalChild;
    cout<<totalChild(5, 6)<<endl;

    Tax high(0.40, 30000); //闭包构造中
    Tax middle(0.25, 20000);
    cout<< "tax over 3w: " << high(37500) <<endl; //将函数类对象作为函数调用,重载()符号
    cout<< "tax over 2w: " << middle(25500) <<endl;

    float tax_rate = 5.5f;
    AirportPrice Changi(tax_rate); //仿函数先通过变量tax_rate初始化仿函数类的私有环境

    auto Changi2 = [tax_rate] (float price) ->float{ return price*(1-tax_rate/100); };
    //lambda函数通过【】捕获局部变量tax_rate,用来设置好闭包环境,两者看起来功能完全一样
    cout<<" A purchased product price sum:"<< Changi(3699)<<endl;
    cout<<" B purchased product price sum:"<< Changi2(2899)<<endl;

    //事实上,现今很多编译器实现lambda都是通过仿函数来实现的,会先将lambda函数转化为一个
    //仿函数对象,因此lambda只是仿函数的一种“syntax sugar”
    //这也是为何在编译lambda函数时,编译器会提示一些构造函数等相关信息,显然是由于lambda
    //的这种实现方式造成的
    //仿函数被广泛应用于STL库中,用来提供给用户自定义操作功能的空间,但是lambda书写简单,可以
    //就地定义使用,故而在一些场景中也可以使用lambda函数代替仿函数
    auto totalChild_func = [](int x, int y) ->int{ return x+y; };
    typedef int (*allChild) (int x, int y);
    typedef int (*oneChild) (int x);

    allChild p;
    p = totalChild_func; //可以将lambda函数转化为函数类型一致的函数指针
   // oneChild q;
   // q = totalChild; //编译失败,参数必须一致

    decltype(totalChild_func) allPeople = totalChild_func;
    cout<<"all child sum: "<<allPeople(girls, boys)<<endl;
    //lambda类型在C++11中被定义为特有的unique、匿名且非联合体unnamed nonunion的class类型
    //decltype(totalChild_func) totalPeople = p; //编译失败,无法将函数指针转换为lambda类型
    //error: conversion from 'allChild {aka int(*)(int, int)}' to non-scalar type   
    //'main():lambda<int, int>' requested

    return 0;

}

这里写图片描述

可以看到仿函数和lambda都可以实现“闭包”特性,即某些环境变量可以运行时动态确定,这是两者相比于普通函数和函数指针最大的优势。以前的普通函数为了应对运行时动态确定,除了多态(重载多态和virtual多态)便是通过template范型编程,而通过lambda引入闭包概念,可以让问题变得更为直观。

Sec4. lambda和STL

Lambda对C++11最大的贡献应该就是改善了STL库的使用。首先来看最经常看的STL算法for_each:

UnaryProc for_each (InputIterator beg, InputIterator end, UnaryProc op)

用一个例子来直观对比下lambda配合for_each的使用

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> nums;

void Add(const int val){
    auto print = [&] {
        for (auto s: nums) {cout<< s << '\t';}
        cout<<endl;
    };

    //传统的for循环方式
    for (auto i = nums.begin(); i != nums.end(); i++)
    {
        *i = *i + val;
    }
    print();

    //试一试for_each及内置仿函数
    //STL内置仿函数plus<int>()仅仅将加法结果返回,并不回写
    for_each(nums.begin(), nums.end(), bind2nd(plus<int>(), val) );
    print();

    //上面采用bind2nd因为不回写,为了将返回结果再次应用于vector nums,通常情况下,我们需要
    //使用transform这个算法,这样transform会遍历nums,并将结果写入nums.begin()给出的首地址
    //实际这里需要使用STL的一个变动性算法:transform
    transform( nums.begin(), nums.end(), nums.begin(), bind2nd(plus<int>(), val) );
    print();

    //相比于此前STL使用下,需要注意for_each和tranfrom的影响不同,这种区别过于晦涩
    //而在lambda下,则一切的逻辑便一目了然,下面函数采用了引用传递,故而会产生回写效果
    //不过在lambda的支持下,我们还是可以使用for_each
    for_each( nums.begin(), nums.end(), [=](int &i){
                            i += val;
              });
    print();
}

int main()
{
    for (int i = 0; i < 10; i++){
        nums.push_back(i);
    }
    Add(10);

    return 0;
}

这里写图片描述

在Effective STL中提到,使用for_each算法相较于手写的循环在效率、正确性、可维护性上都具有一定优势。最典型的,程序员不用关心iterator,或者说循环的细节,只需要设定边界,作用于每个元素的操作,就可以在近似“一条语句”内完成循环,正如函数指针版本和lambda版本完成的那样。

此外,函数指针的应用范围相对较小,尤其是我们需要具备一些运行时才能决定的状态的时候(和上下文紧密联系,即所谓的闭包),函数指针就会捉襟见肘了,倘若回到10年前C++98时代,遇到这种情况的时候,习惯使用泛型编程的coder会毫不犹豫地使用仿函数,不过现在可以可以更优美简洁。可以看到for_each配合lambda可以使得逻辑更为清晰,不像直接使用STL内置仿函数那么晦涩。此外还有一个好处,就是lambda可以提供代码级别的手动整合优化,而使用内置仿函数却做不到合理的循环整合优化。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;
void Stat(vector<int> &v) {
    int errors;
    int score;
    auto print = [&] {
        cout<<"Errors: "<< errors << endl;
        cout<<"Score: " << score << endl;
    };

    errors = accumulate(v.begin(), v.end(), 0);
    score = accumulate(v.begin(), v.end(), 100, minus<int>() );
    print();

    errors = 0;
    score = 100;
    for_each( v.begin(), v.end(), [&](int i){
             errors += i;
             score -= i;
             });
    print();
}

int main()
{
    vector<int> v(10);
    generate(v.begin(), v.end(), []{
             return rand() % 10;
             });
    Stat(v);

    return 0;

}

这里写图片描述
这里可以看到因为lambda将内部逻辑给清晰的展示出来,故而可以将两次循环叠加放在一次循环中。采用accumulate的方式,编译器不能合理地合并循环,我们可能就会遭受一定的性能损失,(比如cache体系的缓冲命中率将会难以提升)。

总结:lambda函数设计的目的,就是就地创造就地使用,使得一段功能可以在一个屏幕里被完整地展示出来,而不需要借助代码浏览工具在文件间找到函数的实现。lambda更倾向于本地轻量级的封装,如果需要全局共享的代码逻辑,还是需要借助函数(无环境状态)或仿函数(有本地环境状态)来封装提供。并且lambda并非用来取代STL内置仿函数,而只是使得STL的学习曲线不再那么陡峭,在接受lambda角度的解释后,再逐步转向使用STL经过调优后的STL算法,这才是正途。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值