C++11 lambda函数

发布时间 2023-08-22 17:51:59作者: 仓隽

在数理逻辑或计算机科学领域中 lambda 是被用来表示一种匿名函数,这种匿名函数代表了一种所谓的λ演算(lambda calculus)。

int main()
{
    int girls=3,boys=4;
    auto totalChild=[](int x,int y)->int{return x+y;};
    return totalChild(girls,boys);
}

该函数接受两个参数(int x, int y),并且返回其和。直观地看,lambda函数跟普通函数相比不需要定义函数名,取而代之的多了一对方括号([])。此外,lambda函数还采用了追踪返回类型的方式声明其返回值。

其余方面看起来则跟普通函数定义一样。

lambda函数的一般形式

而通常情况下,lambda 函数的语法定义如下:

[capture](parameters)mutable->return-type{statement}

其中

  • [capture] : 捕捉列表。捕捉列表总是出现在 lambda 函数的开始处。事实上,[ ] 是lambda 引出符。编译器根据该引出符判断接下来的代码是否是lambda函数。捕捉列表能够捕捉上下文中的变量以供 lambda 函数使用。
  • (parameters) :参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号()一起省略。
  • mutable :mutable修饰符。默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
  • {statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

在lambda函数的定义中,参数列表和返还类型都是可选的部分,而捕捉列表和函数体都
可能为空。那么在极端情况下,C++11中最为简略的lambda函数只需要声明为
[]{};

就可以了。不过理所应当地,该lambda函数不能做任何事情。

捕获列表

int main()
{
    []{};//最简lambda函数
    int a=3;
    int b=4;
    [=]{return a+b;}; //省略了参数列表与返回类型,返回类型由编译器推断为int
    auto fun1=[&](int c){b=a+c;}; //省略了返回类型,无返回值
    auto fun2=[=,&b](int c)->int{return b+=a+c;}; //各部分都很完整的lambda函数
}

直观地讲,lambda函数与普通函数可见的最大区别之一,就是 lambda 函数可以通过捕捉列表访问一些上下文中的数据

具体地,捕捉列表描述了上下文中哪些的数据可以被 lambda 使用,以及使用方式(以值传递的方式或引用传递的方式)。

int main()
{
    int boys=4,int girls=3;
    auto totalChild=[girls,&boys]()->int{return girls+boys;};
    return totalChild();
}

在上面代码中,我们使用了捕捉列表捕捉上下文中的变量 girls、boys。与之前的代码相比,函数的原型发生了变化,即 totalChild 不再需要传递参数。这个改变看起来平淡无奇,此时 girlsboys 可以视为 lambda 函数的一种初始状态,lambda 函数的运算则是基于初始状态进行的运算。这与函数简单基于参数的运算是有所不同
的。
语法上,捕捉列表由多个捕捉项组成,并以逗号分割。捕捉列表有如下几种形式:

  • [var]表示值传递方式捕捉变量var。

  • [=]表示值传递方式捕捉所有父作用域的变量(包括this)。

    多次调用的lambda表达式不要用值捕获,变量传递进来后相当于常量不更新。

  • [&var]表示引用传递捕捉变量var。

  • [&]表示引用传递捕捉所有父作用域的变量(包括this)。

  • [this]表示值传递方式捕捉当前的this指针。

注意

​ 父作用域:enclosing scope,这里指的是包含lambda函数的语句块,在上面的代码中,即main函数的作用域。

通过一些组合,捕捉列表可以表示更复杂的意思。比如:

  • [=,&a,&b] 表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。
  • [&,a,this] 表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其他所有变量。

不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。

  • [=,a]这里=已经以值传递方式捕捉了所有变量,捕捉a重复。
  • [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

在块作用域(block scope,可以简单理解为在{}之内的任何代码都是块作用域的)以外的lambda函数捕捉列表必须为空,在块作用域中的lambda函数仅能捕捉父作用域中的自动变量,捕捉任何非此作用域或者是非自动变量(如静态变量等)都会导致编译器报错。


lambda 与 仿函数

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

int main()
{
    float tax_rate = 5.5f;
    AirportPrice Changi(tax_rate);
    auto Changi2 = [tax_rate](float price)->float
    {
    	return price*(1-tax_rate/100);
    };
    float purchased = Changi(3699);
    float purchased2 = Changi2(2899);
}

在上面代码中,lambda函数捕捉了tax_rate变量,而仿函数则以tax_rate初始化类。其他的,如在参数传递上,两者保持一致。可以看到,除去在语法层面上的不同,lambda和仿函数却有着相同的内涵——都可以捕捉一些变量作为初始状态,并接受参数进行运算。

而事实上,仿函数是编译器实现lambda的一种方式。在现阶段,通常编译器都会把 lambda 函数转化为成为一个仿函数对象。

因此,在C++11中,lambda可以视为仿函数的一种等价形式了,或者更动听地说,lambda是仿函数的“语法甜点”。

注意 

有的时候,我们在编译时发现lambda函数出现了错误,编译器会提示一些构造函数等相关信息。这显然是由于lambda的这种实现方式造成的。理解了这种实现,用户也就能够正确理解错误信息的由来。


lambda 的基础使用

extern int z;
extern float c;
void Calc(int &, int, float &, float);
void TestCalc()
{
    int x, y = 3;
    float a, b = 4.0;
    int success = 0;
    auto validate = [&]() -> bool
    {
        if ((x == y + z) && (a == b + c))
            return 1;
        else
            return 0;
    };
    Calc(x, y, a, b);
    success += validate();
    y = 1024;
    b = 1e13;
    Calc(x, y, a, b);
    success += validate();
}

在没有lambda函数之前,通常需要在TestCalc外声明同样一个函数,并且把TestCalc中的变量当作参数进行传递。出于函数作用域及运行效率考虑,这样声明的函数通常还需要加上关键字static和inline。

相比于一个传统意义上的函数定义,lambda函数在这里更加直观,使用起来也非常简便,代码可读性很好,效果上,lambda函数则等同于一个“局部函数”。

注意

 局部函数(local function,即在函数作用域中定义的函数),也称为内嵌函数(nested function)。局部函数通常仅属于其父作用域,能够访问父作用域的变量,且在其父作用域中使用。

C/C++语言标准中不允许局部函数存在(不过一些其他语言是允许的,比如FORTRAN),C++11标准却用比较优雅的方式打破了这个规则。因为事实上,lambda可以像局部函数一样使用。

lambda 扩展

// 常量的值由它自己初始化状态决定
int Prioritize(int);
int AllWorks(int times)
{
    int i;
    int x;
    try
    {
        for (i = 0; i < times; i++)
            x += Prioritize(i);
    }
    catch (...)
    {
        x = 0;
    }
    
    const int y = [=]
    {
        int i, val;
        try
        {
            for (i = 0; i < times; i++)
                val += Prioritize(i);
        }
        catch (...)
        {
            val = 0;
        }
        return val;
    }();
}

lambda的类型

从C++11标准的定义上可以发现,lambda的类型被定义为“闭包”(closure)的类 ,而每个lambda表达式则会产生一个闭包类型的临时对象(右值)

因此,严格地讲,lambda函数并非函数指针。不过C++11标准却允许lambda表达是向函数指针的转换,但前提是lambda函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有着相同的调用方式。

int main()
{
    int girls = 3, boys = 4;
    auto totalChild = [](int x, int y) -> int
    { return x + y; };
    typedef int (*allChild)(int x, int y);
    typedef int (*oneChild)(int x);
    allChild p;
    p = totalChild;
    oneChild q;
    q = totalChild;                              // 编译失败,参数必须一致
    decltype(totalChild) allPeople = totalChild; // 需通过decltype获得lambda的类型
    decltype(totalChild) totalPeople = p;        // 编译失败, 指针无法转换为lambda
    return 0;
}
int main()
{
    int val;
    // 编译失败,在const的lambda中修改常量
    auto const_val_lambda = [=](){ val = 3; };

    // 非const的lambda,可以修改常量数据
    auto mutable_val_lambda = [=]() mutable{ val = 3; };

    // 依然是const的lambda,不过没有改动引用本身
    auto const_ref_lambda = [&]{ val = 3; };

    // 依然是const的lambda,通过参数传递val
    auto const_param_lambda = [&](int v){ v = 3; };
    const_param_lambda(val);

    return 0;
}

lambda 与 STL

#include <vector>
#include <algorithm>
using namespace std;
vector<int> nums;
vector<int> largeNums;
const int ubound = 10;
inline void LargeNumsFunc(int i)
{
    if (i > ubound)
        largeNums.push_back(i);
}
void Above()
{
    // 传统的for循环
    for (auto itr = nums.begin(); itr != nums.end(); ++itr)
    {
        if (*itr >= ubound)
            largeNums.push_back(*itr);
    }
    // 使用函数指针
    for_each(nums.begin(), nums.end(), LargeNumsFunc);
    // 使用lambda函数和算法for_each
    for_each(nums.begin(), nums.end(), [=](int i)
            {
                if (i > ubound)
                    largeNums.push_back(i); 
            });
}
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
extern vector<int> nums;
void TwoCond(int low, int high)
{
    // 传统的for循环
    for (auto i = nums.begin(); i != nums.end(); i++)
        if (*i >= low && *i < high)
            break;
    // 利用了3个内置的仿函数,以及非标准的compose2
    find_if(nums.begin(), nums.end(),
            compose2(logical_and<bool>(),
                     bind2nd(less<int>(), high),
                     bind2nd(greater_equal<int>(), low)));
    // 使用lambda函数
    find_if(nums.begin(), nums.end(), [=](int i)
            { return i >= low && i < high; });
}
#include <vector>
#include <algorithm>
#include <iostream>
#include <functional>

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及内置仿函数
    for_each(nums.begin(), nums.end(), bind2nd(plus<int>(), val));
    print();
    // 实际这里需要使用STL的一个变动性算法:transform
    transform(nums.begin(), nums.end(), nums.begin(), bind2nd(plus<int>(), val));
    print();
    // 不过在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 1;
}

lambda表达式的开销

C++ Lambda表达式的开销取决于许多因素,例如Lambda的实现方式、编译器的优化能力、Lambda所在的上下文等等。

Lambda表达式在编译时会被转换为一个匿名的函数对象,并且该函数对象可能需要在运行时动态分配和释放内存。因此,Lambda表达式可能会带来一定的运行时开销和内存开销。

在使用Lambda表达式时,如果Lambda表达式只是简单的函数调用或者循环中的一小段代码,那么它的开销可能不会对程序的性能产生显著的影响。但是,如果Lambda表达式被频繁调用或者嵌套在多个循环中,那么它的开销可能会导致程序的性能下降。

另外,Lambda表达式可能会引入额外的编译时间开销,因为编译器需要解析和生成Lambda表达式的代码。但是,现代编译器通常会对Lambda表达式进行优化,以减少其编译时间和运行时开销。

因此,在使用Lambda表达式时,需要根据实际情况进行权衡,考虑Lambda表达式的使用场景和性能影响,并且可以通过一些技巧(如使用std::function、将Lambda表达式定义为静态变量等)来减少Lambda表达式的开销。

总结

C++ 11 中 lambda与仿函数要做好取舍,lambda只能捕获父作用域中的变量,仿函数在某些情况下又很繁杂。