C/C++编程:lamba表达式

1059 篇文章 285 订阅

泛型算法中的定制操作

很多算法都会比较输入序列中的元素,通过定制比较动作,可以控制算法按照编程者的意图工作。比如string类型中缺省排序如下:

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

using namespace std;
int main()
{
    vector<string> v{"This","is", "a", "predicate", ".", "b", "c"};
    sort(v.begin(), v.end());
    for(const auto& s:v){
        cout << s << "\t";
    }
    return 0;
}

代码的输出如下:

缺省动作是按照字母顺序排序的

 谓词

假设有一个需求,希望按照字符串长度从小到大排序。可以先定义一个比较函数

bool compare(const string& s1, const string& s2){
    return s1.size() < s2.size();
}

然后将这个函数传递给sort算法即可

    sort(v.begin(), v.end(), compare);
    for(const auto& s:v){
        cout << s << "\t";
    }

代码的输出如下:

这种作为参数传递给sort算法的函数可以看作一个动作,它有一个名称:谓词

 

lambda表达式

前面的例子中,定义了一个函数传递给sort算法。这个函数可以重复使用还好,如果只是使用一次就比较麻烦。这种情况下可以使用C++22提供的新特性:lamba表达式:

    sort(v.begin(), v.end(),
    [](const string& s1, const string& s2){
                return s1.size() > s2.size();
        });
    for(auto s:v){
        cout << s << "\t";
    }

和使用谓词的情况比较可以看到:

  1. 没有定义函数(没有函数名)。

  2. 依然定义了动作,参数。

这种没有定义函数的指定动作(谓词)的方式就是lamba表达式

 

lamba表达式是可调用对象

可调用对象

对于一个表达式e,如果可以编写代码以e(args)的形式执行它,就可以说e是可调用的。

例如下面的函数:

int add(int a, int b);

可以这样编写代码:

int ret = add(1, 2);

按照前面提到的原则,就可以说add是可调用的。到C++98为止,可调用对象有函数,函数指针,函数对象等。

lamda表达式

C++11之后引入的lambda表达式也是一种可调用对象,其标准形式如下:

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

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

例如可以这样使用lambda表达式:

auto add = [](int a, int b){return a + b;};
cout << add(1, 2) << endl;

可以把lambda表达式通过变量传递,像函数一样调用。当然也可以这样:

int result = [](int a, int b){return a*b;}(2, 3);
cout << result << endl;

可以看到[](int a, int b){return a*b;}这个表达式可以以e{args}的方式被调用,也就是说,这是一个可调用对象。

 

捕捉列表

只管的讲,lamdba函数与普通函数可见的最大区别之一,就是lambda函数可以通过捕捉列表访问上下文中的数据。具体的,捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用方式(以值传递或者引用传递)。

int main(){

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

上面的boys和girls可以视为lambda函数的一种初始状态,lambda函数的运算则是基于初始状态进行的计算。这与函数简单基于参数的运行是不一样的。

 

语法上,捕捉列表由多个捕捉项组成,以,分割。捕捉列表有如下几种形式:

  • [var]表示值传递方式捕捉变量var
  • [=]表示值传递方式捕捉所有父作用域的变量(包括this)
  • [&var]表示引用传递捕捉变量var
  • [&]表示引用传递捕捉所有父作用域的变量(包括this)
  • [this]表示以值传递方式捕捉当前this指针

父作用域:这里指的是包含lamda函数的语句块

通过一些组合,捕获列表可以表达式更复杂的意思,比如:

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

 

不过,值得注意的是,捕捉列表不允许重复传递,否则会导致编译器错误,比如:

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

    int boys = 4, girls = 3;
    auto totalChild = [=]()->int{return boys + girls}; // =捕捉所有父作用域的变量
    return totalChild
}

 

 

函数对象

考虑下面的代码

bool istarget(const string& s){
    return s.size() < 2;
}
vector<string> v{"This","is", "a", "predicate", "."};
auto found = find_if(v.begin(), v.end(), istarget);
cout << *found << endl;

使用find_if算法从给定的vector中找到第一个长度小于2的对象

如果我们希望在istarget中选择string时使用变量而不是固定的2的时候,一般的函数就不能满足需求了(虽然使用全局变量算是一个选项)。这时的一个选择就是函数对象。

实现定义一个重载()运算符的IsTarget类

class IsTarget{
    unsigned int max;
public:
    IsTarget(int m_max):max(m_max){}
    bool operator()(const string s){
        return s.size() < max;
    }
};

IsTarget类可以像下面这样使用:

string test("H");
IsTarget it(2);
cout << it(test) << endl;

注意it(test)的部分,从代码的形式来看,这个类的对象可以以e(args)的形式编码并执行,因此它也是可执行对象。长度信息是在创建对象it时指定,在()运算符被执行是使用。在回到find_if的例子,可以这样使用IsTarget类:

vector<string> v{"This","is", "a", "predicate", "."};
auto found = find_if(v.begin(), v.end(), IsTarget(2));
cout << *found << endl;

在执行find_if时,比较的长度信息作为参数传递给IsTarget类,以便在执行选择操作时使用。

 

使用捕获列表

使用函数对象还是有些麻烦,继续请出lambda表达式,它有一个被[ ]包围的捕获列表,用于捕获lambda表达式所在函数的局部变量:

vector<string> v{"This","is", "a", "predicate", "."};
unsigned int max = 2;
auto found = find_if(v.begin(), v.end(),
          [max](const string& s){return s.size() < max;});

max是在find_if执行的函数里定义的变量,将其包含在lambda表达式的捕获列表[ ]中以后,就可以在lambda表达式中使用它了。

值捕获

先看如下代码:

int factor = 2;
auto multiply = [factor](int value)
                {return factor * value;};
factor = 4;
cout << multiply(2) << endl;

代码中首先为factor赋值2,创建lambda表达式以后,再次赋值4。由于lambda表达式的捕获是在该表达式创建是进行的,而第二次赋值在lambda表达式创建之后,所以muliply(2)的执行结果为4。

引用捕获

还是这段代码,只要在捕获列表中变量的前面多了一个&,就变成了引用捕获。

int factor = 2;
auto multiply = [&factor](int value)
                {return factor * value;};
factor = 4;
cout << multiply(2) << endl;

捕获的时机并没有变化,只是捕获的是factor的引用而不是factor的值,所以定义lambda之后,对factor再次赋值4依然会影响multiply(2)的结果。此时的输出为8。

 

前面例子中使用捕获列表时,具体指定了变量名,属于显示捕获。另外还有隐式捕获,由lambda表达式推断需要捕获的变量。具体方法是:

  • 当需要隐式值捕获时,使用[=];
  • 当需要隐式引用捕获时,使用[&];

可变lambda

假设有如下vector,保存的内容是学生的考试成绩:

vector<int> score{45, 70, 56, 86, 28, 60, 90};

可以用以下代码来寻找第一个及格成绩:

find_if(score.begin(), score.end(), 
        [](int v){return (v >=60);});

如果需要找到第n个及格成绩,很自然地会考虑使用下面的代码:

int counter = 2;
find_if(score.begin(), score.end(), 
        [counter](int v){
           return (v >=60)&&(--counter == 0);
        });

但 是很遗憾,这时会出现编译错误,告诉你counter是只读的。其原因是因为在lambda表达式中很少有需要修改捕获值的场景,因此默认捕获值具有 const属性。如果出现本例这样,确实希望修改捕获值的情况,C++11使用mutable关键字来解决这个问题。来看完整代码:

int counter = 2;
auto iter find_if(score.begin(), score.end(),
        [counter](int v)mutable{
           return (v >=60)&&(--counter == 0);
        });
cout << *iter << endl;

当然了,由于是值捕获,处于lambda表达式外面的counter值依然不会改变。

如果希望连外面的counter一起修改,使用引用捕获即可。

 

lamba表达式

为什么是lambda?

首先这个lambda就是罗马字母λ,lambda表达式即λ表达式。数学上有一个概念叫λ演算,其中的一个内容就是λ表达式。

考虑普通的数学函数表示方法:

f(x) = 2x + 1

按照λ表达式的规则,可以写成:

λx.(2x+1)

这个表达式可以读成“对于参数x,2x+1。这里的Lambda,λ,仅仅表达的是数学中"函数"的概念。

各种编程语言,也引入了λ(lambda)表达式。例如:

C#语言:(x) =>{ return 2x+1; }
Java语言:(x) ->{ return 2x+1; }

C++11中也同样引入了lambada表达式,

[](int x)->int{ return 2 * x + 1;}

对于程序员来讲,lambda表达式提供了一种实现无名函数的方法

无名的烦恼

lambda表达式不需要定义函数(名),在大多数场景下,这是一种便利,但也会带来一些烦恼,例如递归调用。因为没有函数名,如何调用自己就成了一个问题。

当然了,这个问题是可以解决的。这里以阶乘为例进行说明,直接上代码:

function<int(int)> factorial =
        [&](int n){
           if(n < 2) return 1;
           return n * factorial(n - 1);
        };
cout << factorial(3) << endl;

lambda表达式的递归调用有几个要点:

  1. 使用标准库中的function模版类型定义表达式类型,其中模范参数与lambda表达式的返回值,参数一致。

  2. 使用引用捕获来获得factorial的使用权。

  3. 调用factorial实现递归调用。

 

总结:可以认为lambda表达式取得信息有两种方式,或者说两个时机:一个是参数列表,其内容是在表达式被调用时决定;另一个捕获列表,其内容是在是表达式被创建的时候决定

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值