C++冷知识:Lambda表达式

简介: 编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型

什么是Lambda表达式

所谓lambda表达式,是一种为了更方便实现回调和简单逻辑的函数写法,应用于C++ 11的新特性中。


Lambda表达式是一种匿名函数,它可以作为参数传递给其他函数或方法。它通常用于函数式编程,可以简化代码并提高代码的可读性。


lambda 具体的构成分为 不可隐藏部分和可隐藏部分。全量写法是:[capture list](parameter list)->return type function body 速记为:[]()->return {}


详细介绍上面的写法是:


capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为

空):

return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用尾置返回来指定返回类型。

可隐藏部分

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。如下


auto f = []{return 42;);


这里使用了 auto关键字,这是类型推断关键字。用来让编译器自行推断变量类型,简化代码风格。


其次就是简化后的lambda表达式。 这里我们简化了形参和返回类型显式声明。只留下局部变量捕获列表,和函数体。


不可隐藏部分

如上代码所述,局部变量捕获列表,和函数体。是lambda 表达式中不可省略的部分,这里要说明:[]是局部变量捕获列表,全局变量不在这里捕获。详细其他介绍请看下面的分结构解析部分。


{}是函数体部分,这里要说明的是,编译器会通过类型推定决定函数返回类型是什么,如果有除return 外的其他内容,将会认为应当返回void类型。 详细其他介绍请看下面的分结构解析部分。


lambda表达式和函数的区别

Lambda表达式和函数的区别在于,Lambda表达式是一种匿名函数,可以在代码中直接定义和使用,不需要像传统函数一样需要先定义再调用。Lambda表达式通常用于函数式编程,可以简化代码并提高代码的可读性。Lambda表达式可以作为参数传递给其他函数或方法,也可以在函数内部定义和使用。


而函数是一种有名字的代码块,需要先定义再调用。函数通常用于封装一段可重用的代码,可以接受参数并返回值。函数可以在程序的任何地方定义和调用,也可以作为参数传递给其他函数或方法。函数在C++中是一种重要的编程概念,可以帮助我们组织代码并提高代码的可维护性。


让我们换一种通俗的说法吧:

对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。


调用方式

auto f = []{return 42;);
cout << f() << endl; // 打印结果是42


lambda表达式的使用方式是和函数一样的,当然,使用注意细节还是不一致的,比如,lambda表达式是不能在构造、析构函数中使用的,这就让我们最好不要用lambda表达式实现类似于 info()、close()之类的初始化和清理函数了。


分结构解析lambda表达式

[]

这是lambda表达式中必须的部分。

这里被称之捕获,捕获的对象是lambda表达式定义所在的作用域中的局部变量。

在lambda中忽略括号和参数列表等价于指定一个空参数列表。

在此例中,当调用f时,参数列表是空的。

如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void。

这里被称之为捕获,而接下来,我们将要说明一下捕获的种类。


值捕获

同比与值拷贝,是将原值拷贝到新的内存空间中,修改原值时,不会修改拷贝值。


使用方法:[name] {} 这里的name 就是需要捕获的局部变量。


类似参数传递,变量的捕获方式也可以是值或引用。下面表中列出了几种不同的构造捕获列表的方式。


到目前为止,我们的lambda采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝:



void fcn1()
{
    size_t v1=42;//局部变量
    //将v1拷贝到名为f的可调用对象
    auto f  = [v1]{return vl;};
    v1=0;
    auto j=f();//j为42;f保存了我们创建它时v1的拷贝
}


由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。


引用捕获

我们定义lambda时可以采用引用方式捕获变量。例如:


void fcn2()
{
    size t v1=42;//局部变量
    //对象f2包含v1的引用
    auto f2  = [&v1]{return vl;}
    v1=0;
    auto j=f2();//j为0;f2保存v1的引用,而非拷贝
}


v1之前的&指出v1应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。

在本例中,当lambda返回v1时,它返回的是vl指向的对象的值。

引用捕获与返回引用 有着相同的问题和限制。


  • 如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。
  • lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。


引用捕获有时是必要的。

例如,我们可能希望biggies函数接受一个ostream的引用,用来输出数据,并接受一个字符作为分隔符:


void biggies(vector<string> &words,
             vector<string>:size_type sz,
             ostream &os = cout, char c = ' ')
{ 
    //与之前例子一样的重排words的代码
    //打印count的语句改为打印到os
    for_each (words.begin(), words.end(),
        [&os, c](const string &s) {os <<s <<c;});
}


我们不能拷贝ostream对象 ,因此捕获os的唯一方法就是捕获其引用(或指向os的指针)。


当我们向一个函数传递一个lambda时,就像本例中调用for each那样,lambda

会立即执行。在此情况下,以引用方式捕获os没有问题,因为当for_each执行时,

biggies中的变量是存在的。


我们也可以从一个函数返回lambda。函数可以直接返回一个可调用对象,或者返回一

个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,.则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。


隐式捕获

除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。例如,我们可以重写传递给find_if的lambda:


//sz为隐式捕获,值捕获方式
wc =  find_if (words.begin(), words.end(),
                 [=](const string &s){return s.size()>=sz;});
1


如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐

式捕获和显式捕获:


void biggies (vector<string>&words,
              vector<string>:size_type sz,
              ostream &os = cout,char c =' ')
{    
    //其他处理与前例一样
    //os隐式捕获,引用捕获方式;c显式捕获,值捕获方式
    for_each (words.begin(), words.end(),
        [& , c](const string &s)(os << s <<c;});
    //os显式捕获,引用捕获方式;c隐式捕获,值捕获方式
    for each (words.begin(), words.end(),
        [= , &os](const string &s){os << s <<c;});
}


当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值。

当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。


lambda捕获列表


捕获写法 表示含义

[] 空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量后才能使用它们

[name] names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了&,则采用引用捕获方式

[&] 隐式捕获列表,采用引用捕获方式。ambda体中所使用的来自所在函数的实体都采用引用方式使用

[=] 隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值

[&, identifier_list] identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能使用&

[&, identifier_list] identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用&

捕获设置原则

建议:尽量保持lambda的变量捕获简单化

一个lambda捕获从lambda被创建(即,定义lambda的代码执行时)到lambda自身执行(可能有多次执行)这段时间内保存的相关信息。确保lambda每次执行的时候这些信息都有预期的意义,是程序员的责任。

捕获一个普通变量,如int、string或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。

如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值。

在lambda从创建到它执行的这段时间内,可能有代码改变绑定的对象的值。也就是说,在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在lambda执行时,该对象的值可能已经完全不同了。

一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。

()

可隐藏部分,用来说明接受到的其他形参,这里的形参是不同于捕获需要的局部变量的。


他更多的是规定lambda的使用方式,当缺少参数时,它会表示:你不能这样使用我。


lambda表达式没有缺省值。也不能提供默认初始化。


[]是捕获列表,用于指定lambda表达式中使用的外部变量。可以使用[]来捕获外部变量,包括值捕获和引用捕获。值捕获会将外部变量的值复制到lambda表达式中,而引用捕获则会将外部变量的引用传递给lambda表达式。必须通过[]获取的情况包括:


当lambda表达式需要使用外部变量时,需要通过[]来捕获这些变量。

当lambda表达式中使用了this指针时,需要通过[]来捕获this指针。

()是参数列表,用于指定lambda表达式的参数。可以在()中指定lambda表达式的参数类型和名称,多个参数之间用逗号分隔。应当使用()获取的情况包括:


当lambda表达式需要接受参数时,需要通过()来指定这些参数。

->return type

可隐藏部分,规定了返回的类型。

如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void。

在本例中,我们传递给transform一个lambda,它返回其参数的绝对值。lambda体是单一的return语句,返回一个条件表达式的结果。我们无须指定返回类型,因为可以根据条件运算符的类型推断出来。

但是,如果我们将程序改写为看起来是等价的if语句,就会产生编译错误:

//错误:不能推断lambda的返回类型

transform(vi.begin ()vi.end(),vi.begin (),
    [](int i) {if (i < 0) return -i;else return i;});


编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。


当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型


//错误:不能推断lambda的返回类型
transform(vi.begin ()vi.end(),vi.begin (),
    [](int i) -> int {if (i < 0) return -i;else return i;});


在此例中,传递给transform的第四个参数是一个lambda,它的捕获列表是空的,接受

单一int参数,返回一个int值。它的函数体是一个返回其参数的绝对值的if语句。


{}

可变lambda

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变

一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable。因此,可变lambda 能省略参数列表:


void fcn3()
{
    size t v1=42;//局部变量
    //f可以改变它所捕获的变量的值
    auto f =[v1]() mutable {return ++vl;};
    v1=0;
    auto j=f();//j为43
}


一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个cost

类型还是一个非const类型:


void fcn4()
{
    s1zetv1=42;//局部变量
    //v1是一个非const变量的引用
    //可以通过f2中的引用来改变它
    auto f2 =[&v1] {return ++v1;};
    v1=0:
    auto j=f2();//j为1
}


注意

如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。

当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。

总结

lambda 表达式是一种C++11引入了lambda表达式,它是一种匿名函数,可以在代码中直接定义和使用,不需要像传统函数一样需要先定义再调用。Lambda表达式的语法形式为:[capture list] (parameter list) -> return type { function body }。


其中,capture list是捕获列表,用于指定lambda表达式中使用的外部变量;parameter list是参数列表,用于指定lambda表达式的参数;return type是返回类型,用于指定lambda表达式的返回值类型;function body是函数体,用于指定lambda表达式的具体实现。


下面是一个lambda表达式的使用示例:


auto sum = [](int a, int b) -> int { return a + b; }; int result = sum(1, 2); // result = 3


在这个示例中,我们定义了一个lambda表达式,它接受两个int类型的参数,返回它们的和。然后我们使用auto关键字将这个lambda表达式赋值给一个变量sum,最后调用sum函数计算1和2的和,将结果赋值给result变量。


Lambda表达式在C++中的应用场景包括:


STL算法中的函数对象参数,如sort、find_if等;

多线程编程中的回调函数,如std::thread、std::async等;

GUI编程中的事件处理函数,如Qt中的信号槽机制;

函数式编程中的高阶函数,如map、reduce等;

代码中需要频繁定义小型函数的场景,如排序、过滤等。

目录
相关文章
|
3月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
147 44
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
227 12
|
11月前
|
算法 编译器 C++
【C++11】lambda表达式
C++11 引入了 Lambda 表达式,这是一种定义匿名函数的方式,极大提升了代码的简洁性和可维护性。本文详细介绍了 Lambda 表达式的语法、捕获机制及应用场景,包括在标准算法、排序和事件回调中的使用,以及高级特性如捕获 `this` 指针和可变 Lambda 表达式。通过这些内容,读者可以全面掌握 Lambda 表达式,提升 C++ 编程技能。
499 3
|
C++ 算法
c++中 lambda 表达式 解析
c++中 lambda 表达式 解析
150 0
c++中 lambda 表达式 解析
|
算法 编译器 程序员
C++ 11新特性之Lambda表达式
C++ 11新特性之Lambda表达式
92 0
|
安全 编译器 C++
C++一分钟之-泛型Lambda表达式
【7月更文挑战第16天】C++14引入泛型lambda,允许lambda接受任意类型参数,如`[](auto a, auto b) { return a + b; }`。但这也带来类型推导失败、隐式转换和模板参数推导等问题。要避免这些问题,可以明确类型约束、限制隐式转换或显式指定模板参数。示例中,`safeAdd` lambda使用`static_assert`确保只对算术类型执行,展示了一种安全使用泛型lambda的方法。
191 1
C++语言的lambda表达式
C++从函数对象到lambda表达式以及操作参数化
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
114 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
189 0