定义

Lambda 表达式可以说是c++11引用的最重要的特性之一,虽然跟多线程关系不大,但是它在多线程的场景下使用很频繁,所以在多线程这个主题下介绍它更合适。Lambda 来源于函数式编程的概念,也是现代编程语言的一个特点。C++11 这次终于把 Lambda 加进来了,令人非常兴奋,因为Lambda表达式能够大大简化代码复杂度(语法糖:利于理解具体的功能),避免实现调用对象。

Lambda 表达式有如下优点:

  • 声明式编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或者函数对象。以更直接的方式去写程序,好的可读性和可维护性。
  • 简洁:不需要额外再写一个函数或者函数对象,避免了代码膨胀和功能分散,让开发者更加集中精力在手边的问题,同时也获取了更高的生产率。
  • 在需要的时间和地点实现功能闭包,使程序更灵活。

一般有如下语法形式:

auto func = [capture] (params) opt -> ret { func_body; };

其中

  • func:是可以当作Lambda 表达式的名字,作为一个函数使用;
  • capture:是捕获列表;
  • params:是参数列表;
  • opt:是函数选项(mutable, noexcept之类);
  • ret:是返回值类型,可以不写,让编译器根据返回值自动推导;
  • func_body:是函数体。

Lambda 表达式一般用于定义匿名函数,使得代码更加灵活简洁。它就像一个自给自足的函数,也可以不传入函数仅依赖全局变量和函数,甚至都可以不用返回一个值。这样的Lambda表达式的一系列语义都需要封闭在括号中,还要以方括号作为前缀,如下:

[]{  // Lambda表达式以[]开始do_stuff();do_more_stuff();
}();  // 表达式结束,可以直接调用

上面例子中,Lambda表达式通过后面的括号表示直接调用,不过这种方式不常用。因为,如果想要直接调用,可以在写完对应的语句后就对函数进行调用。

在 C++11 中,Lambda表达式的返回值是通过C++返回值类型后置语法来定义的,其实很多时候,返回值也是很简单的,当Lambda函数体包括一个return语句,返回值的类型就作为Lambda表达式的返回类型。如下:

auto x1 = [](int i){ return i; };  // OK: return type is int
auto x2 = [](){ return { 1, 2 }; };  // error: 无法推导出返回值类型

当然我们也可以显式给出具体的返回值类型。

auto x2 = []() -> bool{ return true; };  // return type is bool

虽然简单的Lambda函数很强大,能简化代码,不过其真正的强大的地方在于对本地变量的捕获。

捕获本地变量

Lambda函数使用空的[](Lambda introducer)就不能引用当前范围内的本地变量;其只能使用全局变量,或将其他值以参数的形式进行传递。当想要访问一个本地变量,需要对其进行捕获。最简单的方式就是将范围内的所有本地变量都进行捕获,使用[=]就可以完成这样的功能。函数被创建的时候,就能对本地变量的副本进行访问了。如下:

int a = 0, b = 1;
auto f1 = []{ return a; };               // error,没有捕获外部变量
auto f2 = [=]{ return a + b; };          // OK,捕获所有外部变量,并返回a + b
auto f3 = [=]{ return a++; };            // error,a是以复制方式捕获的,无法修改

这种本地变量捕获的方式相当安全,所有的东西都进行了拷贝,所以可以通过Lambda函数对表达式的值进行返回,并且可在原始函数之外的地方对其进行调用。这也不是唯一的选择,也可以选择通过引用的方式捕获本地变量。在本地变量被销毁的时候,Lambda函数会出现未定义的行为。

下面的例子,就介绍一下怎么使用[&]对所有本地变量进行引用:

int a = 0, b = 1;
auto f1 = [&]{ return a++; };            // OK,捕获所有外部变量的引用,并对a执行自加运算
auto f2 = [&]{ return a + (b++); };      // OK,捕获所有外部变量的引用,并对b做自加运算

这些选项不会让人感觉到特别困惑,你可以选择以引用或拷贝的方式对变量进行捕获,并且还可以通过调整中括号中的表达式,来对特定的变量进行显式捕获。如果想要拷贝所有变量,可以使用[=],通过参考中括号中的符号,对变量进行捕获。下面的例子将会打印出1239,因为i是拷贝进Lambda函数中的,而j和k是通过引用的方式进行捕获的:

#include <iostream>
#include <functional>int main()
{int i=1234,j=5678,k=9;std::function<int()> f=[=,&j,&k]{return i+j+k;};  // 先讲i=1234拷贝到函数内,j和k是引用,调用时决定i=1;j=2;k=3;std::cout<<f()<<std::endl;  // 打印时j和k变成了2和3,所以就是1234+2+3=1239
}

或者,也可以通过默认引用方式对一些变量做引用,而对一些特别的变量进行拷贝。这种情况下,就要使用[&]与拷贝符号相结合的方式对列表中的变量进行拷贝捕获。下面的例子将打印出5688,因为i通过引用捕获,但j和k通过拷贝捕获:

#include <iostream>
#include <functional>int main() {int i=1234,j=5678,k=9;std::function<int()> f=[&,j,k]{return i+j+k;};  // 拷贝j和k的值i=1;j=2;k=3;std::cout<<f()<<std::endl;  // i的引用,1+5678+9=5688
}

如果只想捕获某些变量,可以忽略=或&,仅使用变量名进行捕获就行。加上&前缀,是将对应变量以引用的方式进行捕获,而非拷贝的方式。下面的例子将打印出5682,因为i和k是通过引用的范式获取的,而j是通过拷贝的方式:

#include <iostream>
#include <functional>int main() {int i=1234,j=5678,k=9;auto f=[&i,j,&k]{return i+j+k;};  // 这里可以直接用auto自动推导f类型i=1;j=2;k=3;std::cout<<f()<<std::endl;
}

最后一种方式为了确保预期的变量能捕获。当在捕获列表中引用任何不存在的变量都会引起编译错误。当选择这种方式,就要小心类成员的访问方式,确定类中是否包含一个Lambda函数的成员变量。类成员变量不能直接捕获,如果想通过Lambda方式访问类中的成员,需要在捕获列表中添加this指针。下面的例子中,Lambda捕获this后,就能访问到some_data类中的成员:

struct X {int some_data;void foo(std::vector<int>& vec) {std::for_each(vec.begin(),vec.end(),[this](int& i){i+=some_data;});}
};

并发的上下文中,Lambda是很有用的,其可以作为谓词放在std::condition_variable::wait()std::packaged_task<>中,或是用在线程池中,对小任务进行打包。也可以线程函数的方式std::thread的构造函数,以及作为一个并行算法实现,等待。

C++14后,Lambda表达式可以是真正通用Lamdba了,参数类型被声明为auto而不是指定类型。这种情况下,函数调用运算也是一个模板。当调用Lambda时,参数的类型可从提供的参数中推导出来,例如:

auto f=[](auto x){ std::cout<<”x=”<<x<<std::endl;};
f(42); // x is of type int; outputs “x=42”
f(“hello”); // x is of type const char*; outputs “x=hello”

C++14还添加了广义捕获的概念,因此可以捕获表达式的结果,而不是对局部变量的直接拷贝或引用。最常见的方法是通过移动只移动的类型来捕获类型,而不是通过引用来捕获,例如:

std::future<int> spawn_async_task() {std::promise<int> p;auto f=p.get_future();std::thread t([p=std::move(p)](){ p.set_value(find_the_answer());});t.detach();return f;
}

这里,promise通过p=std::move§捕获移到Lambda中,因此可以安全地分离线程,从而不用担心对局部变量的悬空引用。构建Lambda之后,p处于转移过来的状态,这就是为什么需要提前获得future的原因。

内部原理

编译器为每个Lambda表达式生成如上所述的唯一闭包。注意,这是Lambda表达式的核心所在。捕获列表将成为闭包中的构造函数的参数,如果将参数按值捕获,那么相应类型的数据成员将在闭包中创建。此外,可以在Lambda表达式的参数中声明变量/对象,它们将成为调用operator()函数的参数。如下Lambda表达式:

auto plus = [] (int a, int b) -> int { return a + b; }
int c = plus(1, 2);

编译器将翻译成:

class LambdaClass {
public:int operator () (int a, int b) const {return a + b;}
};LambdaClass plus;
int c = plus(1, 2);

调用的时候编译器会生成一个Lambda的对象,并调用opeartor ()函数。上面是一种调用方式,那么如果我们写一个复杂一点的Lambda表达式,表达式中的成分会如何与类的成分对应呢?我们再看一个 值捕获 例子。

int x = 1; int y = 2;
auto plus = [=] (int a, int b) -> int { return x + y + a + b; };
int c = plus(1, 2);

编译器将翻译成:

class LambdaClass {
public:LambdaClass(int x, int y): x_(x), y_(y) {}int operator () (int a, int b) const {return x_ + y_ + a + b;}private:int x_;int y_;
}int x = 1; int y = 2;
LambdaClass plus(x, y);
int c = plus(1, 2);

其实这里就可以看出,值捕获时,编译器会把捕获到的值作为类的成员变量,并且变量是以值的方式传递的。需要注意的时,如果所有的参数都是值捕获的方式,那么生成的operator()函数是const函数的,是无法修改捕获的值的,哪怕这个修改不会改变lambda表达式外部的变量,如果想要在函数内修改捕获的值,需要加上关键字 mutable。向下面这样的形式。

int x = 1; int y = 2;
auto plus = [=] (int a, int b) mutable -> int { x++; return x + y + a + b; };
int c = plus(1, 2);

我们再来看一个引用捕获的例子:

int x = 1; int y = 2;
auto plus = [&] (int a, int b) -> int { x++; return x + y + a + b;};
int c = plus(1, 2);

编译器的翻译结果为:

class LambdaClass {
public:LambdaClass(int& x, int& y): x_(x), y_(y) {}int operator () (int a, int b) {x_++;return x_ + y_ + a + b;}private:int &x_;int &y_;
};

我们可以看到以引用的方式捕获变量,和值捕获的方式有3个不同的地方:

    1. 参数引用的方式进行传递;
    2. 引用捕获在函数体修改变量,会直接修改lambda表达式外部的变量;
    3. opeartor()函数不是const的。

针对上面的集中情况,我们把lambda的各个成分和类的各个成分对应起来就是如下的关系:

  • 捕获列表,对应LambdaClass类的private成员
  • 参数列表,对应LambdaClass类的成员函数的operator()的形参列表
  • mutable,对应 LambdaClass类成员函数 operator() 的const属性 ,但是只有在捕获列表捕获的参数不含有引用捕获的情况下才会生效,因为捕获列表只要包含引用捕获,那operator()函数就一定是非const函数。
  • 返回类型,对应 LambdaClass类成员函数 operator() 的返回类型
  • **函数体,**对应 LambdaClass类成员函数 operator() 的函数体。
  • 引用捕获和值捕获不同的一点就是,对应的成员是否为引用类型。

Mutable Lambda表达式

通常,Lambda函数的call-operator(调用运算符)隐式为const-by-value(常量,按值捕获),这意味着它是不可变的。 但是函数内部想修改这变量,但是又不想影响lambda表达式外面的值的时候,就直接添加mutable属性,这样调用lambda表达式的时候,会像函数传递参数一样,在内部定义一个变量并拷贝这个值。代码如下所示:

#include <iostream>
using namespace std;int main()
{int t = 9;auto f = [t] () mutable {return ++t; };cout << f() << endl;cout << f() << endl;cout << "t:" << t << endl;return 0;
}

输出:

10
11
t:9

此处值捕获的变量t,它在刚开始被捕获的初始值是9,调用一次f之后,变成了10,再调用一次,就变成了11。
但是最终的输出t,也就是main()函数里面定义的t,由于是值捕获,所以它的值一直不会变,最终还将输出9。

这种情况有点像在函数体中定义了一个static变量接收了值,如下:

auto f = [t]() {static auto x = t;return ++x;
};

Lambda 表达式的类型

lambda 表达式的类型在 C++11 中被称为“闭包类型(Closure Type)”。它是一个特殊的,匿名的非 nunion 的类类型。因此,我们可以认为它是一个带有 operator() 的类,即仿函数。因此,我们可以使用 std::functionstd::bind 来存储和操作 lambda 表达式:

std::function<int(int)>  f1 = [](int a){ return a; };
std::function<int(void)> f2 = std::bind([](int a){ return a; }, 123);

另外,对于没有捕获任何变量的 lambda 表达式,还可以被转换成一个普通的函数指针(必须是没有捕获任何变量):

using func_t = int(*)(int);
func_t f1 = [](int a){ return a; };  // 正确,没有捕获的的lambda表达式可以直接转换为函数指针
f1(123);
func_t f2 = [&](int a){ return a; };  // 错误,有捕获的lambda表达式不能直接转换为函数指针

lambda 表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终均会变为闭包类型的成员变量。而一个使用了成员变量的类的 operator(),如果能直接被转换为普通的函数指针,那么 lambda 表达式本身的 this 指针就丢失掉了。而没有捕获任何外部变量的 lambda 表达式则不存在这个问题。这里也可以很自然地解释为何按值捕获无法修改捕获的外部变量。因为按照 C++ 标准,lambda 表达式的 operator() 默认是 const 的。一个 const 成员函数是无法修改成员变量的值的。而 mutable 的作用,就在于取消 operator() 的 const。

Lambda auto参数

在C++ 14中引入的泛型Lambda,它可以使用auto标识符捕获参数。参数声明为auto是借助了模板的推断机制。如下:

auto func = [] (auto x, auto y) {return x + y;
};
// 上述的lambda相当于如下类的对象
class X {
public:template<typename T1, typename T2>auto operator() (T1 x, T2 y) const { // auto借助了T1和T2的推断return x + y;}
};func(1, 2);
// 等价于
X{}(1, 2);

还可以使用可变泛型,如下:

void print() {}
template <typename First, typename... Rest>
void print(const First &first, Rest &&... args)
{std::cout << first << std::endl;print(args...);
}
int main()
{auto variadic_generic_Lambda = [](auto... param) {print(param...);};variadic_generic_Lambda(1, "lol", 1.1);
}

带可变参数包的Lambda在许多情况下都很有用,如代码调试、不同数据输入的重复操作等。

constexpr Lambda表达式

C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。看下面的例子:

#include <iostream>
#include <functional>int main() { // c++17可编译constexpr auto lamb = [] (int n) { return n * n; };static_assert(lamb(3) != 9, "a");
}

如果使用C++11编译则如下错误:

<source>: In function 'int main()':
<source>:6:27: error: static assertion failed: a6 |     static_assert(lamb(3) != 9, "a");

也可以将 lambda 表达式声明为常量表达式或在常量表达式中使用。

#include <iostream>
#include <string>constexpr int Increment(int n) {auto add1 = [n]()    //Callable named lambda{return n + 1;};return add1();  //call it
}int main() {constexpr int number3 = Increment(2);std::cout << number3 << std::endl;
}

注意:constexpr lambda 表达式有如下限制:函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。

this拷贝

这也是C++17增加的,上面介绍的[this]用法是把对象的引用传给lambda,然而这里的问题是,即使进行了this捕获,也是通过引用捕获了底层对象(只复制了this指针)。如果lambda的生存期超过调用成员函数的对象的生存期,这就会成为一个问题。一个关键的例子是当lambda定义为一个新线程的任务时,该线程应该使用它自己的对象副本来避免任何并发性或生存期问题。

C++17中,我们可以在lambda表达式的捕获类别里[]写上*this,表示传递到lambda中的是this对象的拷贝。从而解决上述的问题。(注:C++11中是不允许这样写的。成员捕获列表中只能是变量、”=“、”&“、”=, 变量列表“、”&, 变量列表“ )

#include <iostream>
#include <string>
#include <thread>class Data {
private:std::string name;
public:Data(const std::string& s) : name(s) {}std::thread startThreadWithCopyOfThis() const {// start and return new thread using this after 3 seconds:std::thread t([*this]{std::cout << "I will shellp 3 seconds" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << name << std::endl;});return t;}
};int main()
{std::thread t;{Data d{ "This copy capture in C++17" };t = d.startThreadWithCopyOfThis();} // d已经销毁std::cout << "the main thread wait for sub thread end." << std::endl;t.join();return 0;
}

lambda中的[*this]就是一个对象的拷贝,这意味着传递了d的一个拷贝。因此,线程在调用d的析构函数后使用传递的对象是没有问题的。
如果我们用[this]、[=]或[&]捕获了,那么线程将运行未定义的行为,因为在传递给线程的lambda中打印name时,lambda将使用已销毁对象的成员。

参考:

http://c.biancheng.net/view/3741.html

C++ 中的 Lambda 表达式 | Microsoft Docs

C++多线程:Lambda表达式相关推荐

  1. Java基础---学Java怎么能不了解多线程---Lambda表达式

    多线程 程序,进程,线程 1.程序(program):一个固定的运行逻辑和数据的集合,是一个静态的概念,一般都存储在磁盘中 2.进程(process):一个正在运行的程序,是一个程序的一次运行,是一个 ...

  2. Java笔记整理五(Iterator接口,泛型,常见数据结构(栈,队列,数组,链表,红黑树,集合),jdk新特性,异常,多线程,Lambda表达式)

    Java笔记整理五 1.1Iterator接口 Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象 ...

  3. 【Java Lambda表达式】Lambda表达式详解、Lambda表达式的等效使用方式、多线程

    1.静态内部类 静态内部类,属于类中的类,好处是:如果不使用,就不会被编译. 如果把这个类放到方法中,成为局部内部类(看下一部分) package cn.hanquan.test;/*Lambda表达 ...

  4. 五、Java中常用的API(通过包进行分类)————异常、多线程和Lambda表达式

    之前已经介绍了java.lang包下的相关类,今天将要补充两个常用的API:java.lang.Throwable和java.lang.Thread 一.异常(java.lang.Throwable) ...

  5. Java学习记录五(多线程、网络编程、Lambda表达式和接口组成更新)

    Java学习记录五(多线程.网络编程.Lambda表达式和接口组成更新) Java 25.多线程 25.1实现多线程 25.1.1进程 25.1.2线程 25.1.3多线程的实现 25.1.4设置和获 ...

  6. 多线程-静态代理-Lambda表达式

    文章目录 多线程 进程和线程 (Process and Thread) 线程 实现线程的三种方式 继承Thread类 实现Runable接口 实现Callable接口 静态代理 Thread底层实现方 ...

  7. 多线程、线程池以及Lambda表达式的总结笔记分享

    文章目录 1. 多线程技术 1.1 线程与进程 1.2 守护线程和用户线程 1.3 线程的六种状态 1.4 线程的调度 1.5 同步与异步 1.6 并发与并行 1.7 Thread类 1.7.1 Th ...

  8. 【JAVA黑马程序员笔记】四 P314到P384(特殊流、多线程编程、网络编程模块、lambda表达式、接口组成更新、方法引用、函数式接口)

    P314-315 字节/符打印流 PrintStream ps = new PrintStream("test.txt");//使用字节输出流的方法ps.write(97);// ...

  9. lambda表达式——写多线程

    JDK1.8 中Lambda 表达式的出现,基本可以取替原来的匿名类实现多线程的方式.下面列举常用的常用的三种情况. 一.普通开启异步线程 new Thread(() -> System.out ...

最新文章

  1. Inside Linux kernel
  2. Markdown学习测试.md
  3. FlexoCalendar周日历出错的解决方法
  4. 恢复xfs文件系统superblock实验
  5. linux图形界面编程基本知识
  6. 世界上最好用的浏览器Chrome 10周岁生日,迎来一大波更新!
  7. 新版 Windows 10 最佳功能预览,五月即将更新
  8. python 实例化过程_python实例化对象的具体方法
  9. Wpf之Tree使用Dictionary作为数据源
  10. Delphi Sql语句中值的引用
  11. 蓝桥基础练习 杨辉三角形 JAVA
  12. 超大文本文件浏览器Snaptext,支持不限制大小的文本文件浏览
  13. 【文献研究】国际班轮航运的合作博弈:The coopetition game in international liner shipping
  14. POJ 3744 Scout YYF I:概率dp
  15. 计算机显示网络无权限访问权限,小编教你电脑显示无internet访问权限怎么办
  16. 数据结构(C#)_排序算法(冒泡排序)
  17. 云计算入门教程普通用户
  18. 用Tortoise SVN抽取补丁包(patch)
  19. 从零开始学习使用VUE搭建一个管理系统页面
  20. 【蓝桥杯嵌入式主板G4】第五章 利用Delay函数来实现LED的闪烁

热门文章

  1. css样式设置文本框为只读,css怎么将文本框设置为只读
  2. 算法学习——递推之水手分椰子
  3. APP安装包为什么这么大
  4. c语言中爱心符号,爱心符号的由来?
  5. mysql8.0.25升级到mysql8.0.30
  6. javaScript中使用sort方法给数组和数组对象进行排序( 比值函数排序)
  7. 自动合patch脚本
  8. React-router4简约教程
  9. 解析SAT阅读考试题的特点
  10. 在线测试计算机本科论文,在线考试系本科毕业论文.doc