Lambda表达式

各位国庆节快乐,祝祖国繁荣昌盛!

常见的语言中都提供Lambda语法糖,比如C#, Python, Golang等。本文将探讨下C++ 11引入的Lambda语法糖。语法糖是一种让程序员使用更加便利的一种语法,并不会带来额外的功能,比如Lambda,没有这种语法糖,其可以用已有的语法等价的实现出相应的功能。 有编程实践经验的同学一定能够快速的理解Lamdba产生的意义,而缺乏编程经验的同学,跟着我一起来梳理下Lamdba给我们带来了哪些便利性?

函数指针和对象函数

因为笔者用Lambda最多的场景是回调函数,先说说回调函数。在编程中回调函数是一个常见的设计方式, 下图是一个常见的同步调用的回调函数:

  1. 调用方访问被调用方的实现函数InvokeFunction
  2. 被调用方访问调用方的回调函数CallbackFunction

上述是一个同步调用的回调方式,是实践中,也有可能是一个异步的回调方式。 一般回调的使用场景可以是被调用方使用调用方指定的方法去实现内部的一个逻辑。常见的比如:

  1. 被调用模块使用调用模块指定的方法完成其功能,比如常见的std::sort
  2. 比如SDK没有写DebugLog的功能,而是通过回调函数的方式,让调用方实现写DebugLog功能。
  3. 通知机制:在一些场景下,被调用方通过回调函数去通知调用模块,去进行相应操作。

回调的场景应该不止上述描述的这些,这一章节的重点让我们回归到回调函数函数对象仿函数)。

回调函数最常见的C和C++中都使用的函数指针,我们以std::sort为例。一个vector容器中存储了若干的Student信息,想要将这些学生信息根据年龄进行升序排序,于是可以调用std::sort,并且使用自定义的函数StudentSortFunctionsort作为回调函数来完成排序。

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

struct Student
{
  std::string  m_strName;
  unsigned int m_uAge;
};

void PrintStudentVector(const std::vector<Student>& vecStudents)
{
  for (auto&& student : vecStudents)
  {
    std::cout << student.m_strName.c_str() << ":" << student.m_uAge << std::endl;
  }
  std::cout << std::endl;
}

bool StudentSortFunction(const Student& student1, const Student& student2)
{
  return student1.m_uAge < student2.m_uAge;
}

int main()
{
  std::vector<Student> vecStudents= {
    {"xiaoqiang", 15}, 
    {"xiaoming", 13},
    {"xiaoke", 13}
  };

  PrintStudentVector(vecStudents);

  std::sort(vecStudents.begin(), vecStudents.end(), StudentSortFunction);

  //Print after sort
  PrintStudentVector(vecStudents);
  return 0;
}

C++中有了函数对象概念,我们同样以上述的例子,实现了一个函数对象StudentSort,其包含一个重载的函数接口bool operator() (const Student& student1, const Student& student2),同样可以实现同样的功能。

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

struct Student
{
  std::string  m_strName;
  unsigned int m_uAge;
};

void PrintStudentVector(const std::vector<Student>& vecStudents)
{
  for (auto&& student : vecStudents)
  {
    std::cout << student.m_strName.c_str() << ":" << student.m_uAge << std::endl;
  }
  std::cout << std::endl;
}

class StudentSort
{
public:
  bool operator() (const Student& student1, const Student& student2)
{
    return student1.m_uAge < student2.m_uAge;
  }
};

int main()
{
  std::vector<Student> vecStudents= {
    {"xiaoqiang", 15}, 
    {"xiaoming", 13},
    {"xiaoke", 13}
  };

  PrintStudentVector(vecStudents);

  std::sort(vecStudents.begin(), vecStudents.end(), StudentSort());

  //Print after sort
  PrintStudentVector(vecStudents);
  return 0;
}

当然上述的例子函数指针函数对象似乎没有太多区别。我们注意看回调的方法的入参是由被调用方给定的并且传入的。但是在一些场景,我们是需要在回调方法中同样传入被调用方的一些信息。这个时候的回调方法一般的形式是, 会传入一个pCtx,其存储调用方所需要传递给回调函数的一些信息。

void CallbackFunction(Contex* pCtx, Parameter par1, Parameter par2.....)

在这种情况下函数指针函数对象就有了区别了,函数指针是没有成员的,而函数对象是可有成员函数的,这个时候在C++中,回调的方法一般采用函数对象来实现上述的方式, 比如定义了一个回调函数对象CallbackContext callbackContext设置给被调用方被调用方使用callbackContext(par1, par2)即完成了回调方法的调用。

class CallbackContex
{
public:
  bool operator() (Parameter par1, Parameter par2) { ; };
private:
  Contex* m_pCtx;
};

那么也就是说,每次我们设置给被调用放都需要定义一个class,将调用方需要设置给被调用方的变量给打包到一个叫做Contex中,这个时候手写一个函数对象,感觉比较繁琐。注意只是繁琐,而不是无法实现。 这个时候使用Lambda来实现就显的十分的方便快捷了,因为其有一个很棒的功能,叫做捕获变量。接下来让我们一起来看看本文的主角lambda吧。

Lambda

Lambda的表达式如上图所示,其主要构成部分就比普通的函数多了一个捕获列表,主要由5个部分构成。

  1. 捕获列表,其可以捕获当前上下文的变量,可以是值捕获或者引用捕获
  2. 函数参数,不用赘述,和普通函数一样
  3. specifiers, 可选的,主要说明下mutable, 默认情况下值捕获,将无法修改其值(可以想象为其成员函数后面跟了个const),除非设置为mutable.
  4. 返回值,如果不写表示返回void
  5. 函数体, 这部分可以使用你捕获列表里面的变量,也可以使用参数列表里面的变量。

看到这里是不是来演练下第一章节的例子,使用Lambda如何更简洁的写出一个排序的回调, 是不是比较简单。

std::sort(vecStudents.begin(), vecStudents.end(), [](const Student& student1, const Student& student2) -> bool {
    return student1.m_uAge < student2.m_uAge;
  });

Lambda的表达式的结果(注意不是返回值)是一个匿名函数对象,我们一般可以使用 auto来获取其表达式结果,同样也可以使用。std::function<T>。 下面我们来举个例子让我们来更加好的理解Lambda, 尤其是值捕获引用捕获

#include <iostream>

int main()
{
  unsigned int uYear = 2020;
  unsigned int uMonth = 9;

  std::cout << "uYear: " << uYear
    << " Month: " << uMonth << std::endl << std::endl;

  auto lambda = [&uYear, uMonth]() -> bool {
    uYear = 2021;
    std::cout << "uYear: " << uYear
      << " Month: " << uMonth << std::endl << std::endl;

    //error C3491: 'uMonth': a by copy capture cannot be modified in a non-mutable lambda
    //uMonth = 10;

    return true;
  };
  
  lambda();

  std::cout << "uYear: " << uYear
    << " Month: " << uMonth << std::endl << std::endl;
  return 0;
}

这个例子我们可以看到在Lambda中使用引用捕获uYear, 值捕获uMonth。那么在Lambda函数体内:

  • uYearmain函数中的uYear的引用,对uYear的重新复制为2021也会影响到mainuYear
  • uMonth只是main函数中的uMonth的值传递,默认情况下不能够直接进行改写,除非将Lambda指定为mutable。如果其为mutable, 在函数体内的修改并不会影响mainuMonth的改变。

其实上述的Lamdba表达式可以用下面的类来表达其含义, 这样的表达易于读者去理解Lambda在编译器中的实现,也能够更好的掌握Lambda

class LambdaClass_XXXXX
{
public:
  LambdaClass_XXXXX(unsigned int& uYear, unsigned int uMonth) :m_uYear(uYear), m_uMonth(uMonth) {}
  bool operator()() const
{
    m_uYear = 2021;
    std::cout << "uYear: " << m_uYear
      << " Month: " << m_uMonth << std::endl << std::endl;

    return true;
  }
private:
  unsigned int& m_uYear;
  unsigned int  m_uMonth;
};

LambdaClass_XXXXX的命名方式是避免的名字冲突。实际可以查看编译器MSVC命名的方式如下图所示:

如果有很多的参数需要捕获,Lambda也提供了一些简便的方式:

  • [&, uMonth] 表示uMonth采用值捕获,其他可见的变量均采用`引用捕获
  • [=, &uYear] 表示uYear采用引用捕获,其他可见的变量均采用值捕获

那么如果捕获列表的变量名字和函数参数名字相同呢? ,我试了几个不同的编译器,结果不相同,有的报错,有的优先选择函数参数,有的优先选择捕获列表。总之使用者尽量避开名字相同的问题。关于这个在Stackoverflow上也有所讨论: <<Lambda capture and parameter with same name - who shadows the other? (clang vs gcc)>>, 个人的角度来说更希望是编译阶段直接报错。

通过这一章节的内容,你是否能够举一反三了呢?出一道题目给读者做一做吧。

给读者的问题

为了更好的让读者理解Lambda的实现,请问以下的程序结果输出是什么呢?先想一个答案,然后不确定的同学用编译器跑了试一试吧。如果答案错误,欢迎和笔者一起讨论哦。

#include <iostream>

int main()
{
  int iVal = 100;
  auto lambda = [iVal]() mutable {
    iVal += 100;
    std::cout << iVal << std::endl;
  };
  lambda();
  lambda();
  return 0;
}

总结

Lambda是一种让C++对象函数编写更加便利的语法糖,在使用Lambda的时候一定要理解其实现原理,尤其是捕获列表值捕获引用捕获, 以及要注意其生命周期,以防非法的内存访问导致程序出错。另一点就是文中提到的一个注意点,尽量避免捕获列表的变量名称和函数参数的变量名称相同的情况,因为当前的不同编译器的实现不同,否则掉进坑里了哦。

本文分享自微信公众号 - 一个程序员的修炼之路(CoderStudyShare),作者:河边一枝柳

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-10-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • lambda表达式

    瑾诺学长
  • Lambda表达式

    函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,下面举例多线程的Runnable接口

    晚上没宵夜
  • Lambda表达式

    Java中一切皆对象,因此在Java中函数或者方法无法独立存在,它们不是一个对象,要想像JavaScript进行函数式编程,Java8提出Lambda表达式。

    Krains
  • Lambda表达式

    当需要启动一个线程去完成任务时,通常会通过 java.lang.Runnable 接口来定义任务内容,并使用java.lang.Thread 类来启动该线程。代...

    咕咕星
  • Lambda表达式

    这里我们添加了一些自定义代码到 Schedule 监听器中,需要先定义匿名内部类,然后传递一些功能到 onSchedule 方法中。

    一觉睡到小时候
  • Lambda 表达式

    Java Lambda 表达式是一种匿名函数;它是没有声明的方法,即没有访问修饰符、返回值声明和名字。

    希希里之海
  • Lambda表达式

    Lambda表达式是可以在函数式接口上使用的。函数式接口就是只定义一个抽象方法的接口。比如:

    后端码匠
  • java lambda表达式

    作者:Jakob Jenkov 译者:java达人 来源:http://tutorials.jenkov.com/java/lambda-expressio...

    java达人
  • C# lambda表达式

    学了N多久的委托,终于告一段落,现在可以开始lambda的学习之旅了,但是在说lambda之前必须先说下C#中的匿名方法. 1、匿名方法 下面是一个字符串拼接的...

    郑小超.
  • Python lambda表达式

    “Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lam...

    Steve Wang
  • Java lambda表达式

    lambda表达式是一段可以传递的代码,它的核心思想是将面向对象中的传递数据变成传递行为。

    赵哥窟
  • C++:Lambda表达式

    1. 匿名函数概念2. Lambda 表达式的表示3. Lambda 表达式各部分3.1 Capture 子句3.1.1 引用捕获3.1.2 值捕获3.1.3 ...

    王强
  • C++:Lambda表达式

    1. 匿名函数概念2. Lambda 表达式的表示3. Lambda 表达式各部分3.1 Capture 子句3.1.1 引用捕获3.1.2 值捕获3.1.3 ...

    王强
  • Java8 lambda表达式

    前些天在写代码时,突然发现某一位大佬的代码中充斥着stream来操作List,自己的for循环相比之下黯然失色,遂决定要尽快学习一下。接下来突然的一周加班阻塞了...

    呼延十
  • Python Lambda 表达式

    tonglei0429
  • Java8 Lambda表达式

    在Python中是有的。但是Python中万物皆对象,直接将函数赋值给一个变量即可,那么在Java中该如何使用lambda表达式呢?

    烟草的香味
  • 【Java_17】Lambda 表达式

    用户8250147
  • Java Lambda表达式

    在了解Lambda表达式之前我们先来区分一下面向对象的思想和函数式编程思想的区别 面向对象的思想: 做一件事情,找一个能解决这个事情的对象,调用他的方法来解...

    一只胡说八道的猴子
  • Android-Lambda表达式

    Lambda,中文名“兰布达”。是匿名函数的别名,Java8后开始引入Lambda表达式.而Android方面Android Studio 2.4 Previe...

    android_薛之涛

扫码关注云+社区

领取腾讯云代金券