C++11:lambda表达式

这篇具有很好参考价值的文章主要介绍了C++11:lambda表达式。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 概念

lambda表达式实际上是一个匿名类的成员函数,该类由编译器为lambda创建,该函数被隐式地定义为内联。因此,调用lambda表达式相当于直接调用它的operator()函数,这个函数可以被编译器内联优化(建议)。

例如快速排序算法,STL允许用户自定义比较方式,在C++11之前,通常使用仿函数实现。但是代码一旦很长,使用之处和仿函数实现的地方相隔甚远,而且如果仿函数的命名不规范,很容易造成使用上的困难。

仿函数,又叫做函数对象(functor,function object),因为实现仿函数的方式就是重载一个类的operator(),只是用起来跟函数一样,其本质依然是一个对象。

2. 语法

C++11的lambda表达式是一种允许内联函数的特性,它可以用于不需要重用和命名的代码片段。lambda表达式的一般形式是:

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

以一个简单的例子理解lambda表达式的语法:

[]
{
    cout << "hello lambda" << endl;
}

这一大坨就表示它是一个类型/对象(type、class),我们知道,在类型后加上(),就表示调用这个对象的构造函数,**而lambda的语法有些不同,在它后面加上括号,就表示调用它。**就像:

#include <iostream>
using namespace std;

int main()
{
    []
    {
        cout << "hello lambda" << endl;
    }();
    
    return 0;
}

输出:

hello lambda

而一般没有人会这样调用lambda表达式,因为这样还不如直接写中间的语句,所以可以暂时给它一个名字:

auto l = []
{
    cout << "hello lambda" << endl;
};

l在这里是任意取的名字(实际上要根据含义),刚才提到,方括号和后面一坨东西,就相当于一个对象,一般情况下我们是不知道它的类型的,所以通常用auto接收。

现在,最简形式的lambda表达式就写出来了,试着调用它:

#include <iostream>
using namespace std;


int main()
{
    auto l = []
    {
        cout << "hello lambda" << endl;
    };
    l();
    return 0;
}

输出:

hello lambda

注意:

  • 上面的例子和概念中的“内联”属性并没有关系。
  • lambda表达式在main函数之外定义也不影响结果。
  • lambda表达式的最后有分号。
  • lambda函数的参数列表和返回值类型不是必要的,但捕捉列表和函数体是不可省略的。习惯上不写返回值,让表达式自动推导。

3. 示例

现在,lambda表达式的基本用法已经有大概的了解了,下面将介绍它的细节。

示例1

int main()
{
    int id = 0;
    auto f = [id] () mutable
    {
        cout << "id:" << id << endl;
        ++id;
    };
    id = 42;
    f();
    f();
    f();

    cout << "id:" << id << endl;
}

注:在这里先不管mutable

  • [id]表示在lambda表达式外部的所有名为id的变量都能被使用
  • ()表示这个lambda表达式不用传参
  • 函数内部的语句打印变量id的值且累加
  • 在更新变量id的值为42后,调用3次lambda表达式f
  • 最后再打印变量id的值

输出:

id:0
id:1
id:2
id:42

这与设想的结果应该不同:既然变量id的值已经被更新为42,那么lambda表达式f打印出的值应该是42、43、44,这是为何呢?这个问题会在看完所有例子以后揭晓答案。

通过例1,最重要的是理解lambda的本质是一个仿函数,例1的lambda表达式等价于下面的代码:

lambda表达式c++11,C++,c++,开发语言

  • [id]中没有任何特殊符号,表示默认以传值方式捕捉变量,也就相当于拷贝一份变量id的副本到这个匿名函数中,且捕捉后不再受外界变量id的影响
  • mutable表示可以修改捕捉的变量,那么也就可以执行自增语句
  • 例1按照语句的执行顺序,在执行lambda表达式时,变量id的值为0时就已经被捕捉了,即使它被更新为42,后续调用lambda表达式时变量id仍然从0开始

但是,即使lambda表达式内部对变量id自增,最终变量id仍然为最后一次修改的值:42。

示例2

int main()
{
    int id = 0;
    auto f = [&id] (int param)
    {
        cout << "id:" << id << ", ";
        cout << "param:" << param << endl;
        ++id;
        ++param;
    };

    id = 42;
    f(7);
    f(7);
    f(7);

    cout << "id:" << id << endl;
    return 0;
}

输出:

id:42, param:7
id:43, param:7
id:44, param:7
id:45

例2和例1的区别:去掉了mutable,以传引用方式捕捉变量,新增参数param。

  • [&id],表示以引用方式捕捉变量id。那么匿名函数内部和外界处理的变量id就是同一个,函数内部和外部都会互相影响。因此在调用lambda表达式之前变量id被更新为42仍然会影响lambda表达式内部。
  • 参数param是值传递,每次传值都是一次临时拷贝。

示例3

int main()
{
    int id = 0;
    auto f = [id] ()
    {
        cout << "id:" << id << ", ";
        ++id; // 编译错误
    };

    id = 42;
    f();
    f();
    f();

    cout << "id:" << id << endl;
    return 0;
}

在自增语句编译错误:

error: cannot assign to a variable captured by copy in a non-mutable lambda

提示不能赋值给一个非可变的(non-mutable)变量。结合例1,言外之意是mutable的修饰使得lambda表达式能够在内部修改捕捉到的变量。

示例4

int main()
{
    int id = 0;
    auto f = [id] () mutable
    {
        cout << "id:" << id << endl;
        ++id;

        static int x = 5;
        int y = 6;
        return id;
    };

    f();
    return 0;
}

此例仅为了说明lambda表达式本质是一个匿名函数(底层由仿函数实现),所有函数的内部都能定义不同类型的变量,也允许有返回值。

4. 捕捉方式

基本方式

Lambda表达式的捕捉方式是指它如何访问外部变量,也就是定义在lambda表达式之外的变量。Lambda表达式的最基本的两种捕获方式是:按值捕获(Capture by Value)和按引用捕获(Capture by Reference)。

  • 按值捕获是指lambda表达式内部保存一份外部变量的副本,对这个副本进行操作不会影响原来的变量。

  • 按引用捕获是指lambda表达式内部直接使用外部变量的引用,对这个引用进行操作会影响原来的变量。

隐式和混合

除此之外,还有两种方式:隐式捕获和混合方式。它们是两种更简便的捕获方式,它们可以让编译器自动推断需要捕获的外部变量。

隐式捕获:

  • [=]表示以值捕获的方式捕获所有外部变量(默认),成员函数包括this指针;
  • [&]表示以引用捕获的方式捕获所有外部变量,成员函数包括this指针。

混合方式是指在隐式捕获的基础上,显式地指定某些变量的不同捕获方式。

混合使用时,要求捕获列表中第一个元素必须是隐式捕获(&或=),然后后面可以跟上显式指定的变量名和符号。

混合使用时,若隐式捕获采用引用捕获&,则显式指定的变量必须采用值捕获的方式;若隐式捕获采用值捕获=,则显式指定的变量必须采用引用捕获的方式,即变量名前加&。

一个例子:

int main()
{
    int a = 123;
    int b = 456;
    auto f = [=, &b]() { // 隐式值捕获a,显式引用捕获b
        cout << a << endl; // 输出:123
        cout << b << endl; // 输出:456
        b = 789; // 修改b的值
    };
    f();
    cout << b << endl; // 输出:789
    
    return 0;
}

输出:

123
456
789

补充

  • [=]以值捕获,是默认状态一般不写。

  • 不允许以相同方式重复捕捉,例如[=, a],前者已经表示以值捕获所有外部变量,后者就重复了。但是[=, &a]是被允许的,因为它们的捕获方式不同。

  • Lambda表达式的父作用域是指定义lambda表达式的那个作用域,通常是一个函数或者一个类。Lambda表达式的块作用域是指lambda表达式本身的作用域,它是一个匿名函数,可以在其他地方调用。

  • Lambda表达式可以捕获父作用域中的局部变量,但是不能捕获其他作用域或者非局部变量。Lambda表达式捕获父作用域中的局部变量时,要求这个变量必须是声明为final的,或者实际上是final的(即不会被修改)。Lambda表达式不能在自己的块作用域中声明和父作用域中同名的参数或者局部变量。

  • Lambda表达式之间不能相互赋值,因为它们的本质是函数对象。即使从文本层面看它们的名字相同,但是底层编译器给它们取的名字是不同的。

5. 传递lambda表达式

C++传递lambda表达式的方法有以下几种:

  • 将lambda表达式转换为相应的函数指针(如果没有捕获任何变量)。使用std::function作为参数类型,它可以接受任何可调用对象(包括捕获变量的lambda表达式)。例如:
void foo(std::function<bool(int)> f) 
{
  // 函数动作
}

foo(
    [] (int x) // 传递lambda表达式
    { return x > 0; 
    }
); 
  • 将lambda表达式赋值给一个变量,然后将这个变量传递给函数。例如:
auto add = [] (int a, int b) // 将lambda表达式赋值给add变量
{ 
    return a + b; 
}; 

int result = add(1, 2); // 调用lambda表达式
bar(add); // 传递lambda表达式给变量bar
  • 使用decltype来推导出lambda表达式的类型,然后将这个类型作为模板参数传递给函数。例如:
template<typename F>
void baz(F f) 
{
  // 函数动作
}

auto mul = [] (int a, int b) 
{
    return a * b; 
}; // 定义lambda表达式

baz<decltype(mul)>(mul); // 传递lambda表达式和它的类型给变量baz

示例

下面以第三种方式为例:

int main()
{
    auto cmp = [](int a, int b)
    {
        return a > b;
    };

    set<int, decltype(cmp)> s(cmp);
    
    return 0;
}

这段代码中使用了decltype是为了推断出lambda表达式的类型。decltype是一个C++11引入的关键字,它可以根据表达式的类型返回一个类型。在这个例子中,decltype(cmp)就是lambda表达式的类型,它被用作set容器的第二个模板参数,指定了set中元素的比较方式。set的第三个模板参数需要一个比较函数类型,而lambda表达式本身没有一个固定的类型,decltype可以根据lambda表达式的operator()来推导出它的函数类型,从而满足set的要求。

如果不使用decltype,就需要显式地写出lambda表达式的类型,但这很麻烦,因为lambda表达式的类型是编译器生成的,并没有一个具体的名字。你就需要自己定义一个比较类或者函数,并将其作为模板参数传递给std::set。

在这里暂不讨论泛型lambda表达式。

6. 原理

在上文已经提到,lambda表达式的本质就是一个函数对象,即仿函数,下面分别是两个功能相同的lambda表达式和函数对象:

class Add
{
public:
    Add(int base)
            :_base(base)
    {}
    int operator()(int num)
    {
        return _base + num;
    }
private:
    int _base;
};
int main()
{
    int base = 1;

    // 函数对象
    Add add1(base);
    add1(1);

    // lambda表达式
    auto add2 = [base](int num)
    {
        return base + num;
    };
    add2(1);
    
    return 0;
}

lambda表达式c++11,C++,c++,开发语言

网站链接:https://godbolt.org/

通过汇编代码,可以看到编译器处理lambda表达式的方式和处理函数对象相同。原因是C++的lambda表达式底层实现是一个类,它重载了operator()运算符,使得它可以像函数一样被调用。这个类还包含了lambda表达式捕获的变量,它们可以是值捕获或引用捕获。

一个lambda表达式产生一个临时对象,它可以用来初始化一个栈上的变量。这个对象有构造函数和析构函数,并且遵循所有C++规则。

这就是lambda表达式即使在上层看来名字相同也不能相互赋值的原因,编译器在底层给它们各自的函数对象的类起了不同的名字。例如在VS编译器中,具体类名为:lambda_uuid

类名的 uuid (Universally Unique Identifier)是一个用于标识类的唯一标识符。

lambda_uuid即lambda表达式的类型。所以lambda表达式的“匿名”属性是对于使用者而言的,对于编译器来说是有名的。

7. 内联属性

C++的lambda表达式是一种方便的定义匿名函数对象(Go语言中的闭包)的方法,它可以在调用或作为参数传递给函数的地方直接定义。通常lambda表达式用于封装一些传递给算法或异步函数的代码。

C++11中引入了λ表达式,它可以用来定义一个**内联 (inline)**的函数,作为一个本地的对象或者一个参数。内联函数是一种编译器优化技术,它可以避免函数调用的开销,提高执行效率。λ表达式可以被编译器自动内联展开,从而减少函数调用的次数。但是,并不是所有的λ表达式都会被内联,这取决于编译器的实现和优化策略。

一般来说,lambda表达式会被内联的条件是:

  • lambda表达式作为参数传递给一个内联函数
  • lambda表达式没有被保存或者传递到其他地方
  • lambda表达式没有从更外层的函数返回

如果不满足这些条件,可以使用noinline关键字来标记lambda参数,表示不要求内联。另外,编译器也会根据实现和优化策略来决定是否内联lambda表达式。

如果一个函数被调用的频率很低,甚至只有一次,那么内联属性使得它在被调用的位置展开,能减少调用函数创建栈帧的开销。lambda表达式的内联属性是默认的,只要编译器认为它可以在某处展开,那么它就像一个内嵌语句一样。文章来源地址https://www.toymoban.com/news/detail-630835.html

到了这里,关于C++11:lambda表达式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++11_lambda表达式

    lambda表达式是C++11新引入的功能,它的用法与我们之前学过的C++语法有些不同。 [capture-list] (parameters) mutable - return-type { statement } [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的

    2024年02月02日
    浏览(49)
  • C++11新特性lambda 表达式

    Lambda 表达式的基本语法是:[] (参数列表) - 返回值类型 {函数体}。 方括号([])表示捕获列表,用来指定在 lambda 表达式中可以访问的外部变量。 参数列表和返回值类型与普通函数的参数列表和返回值类型相同。 函数体则是实际的代码逻辑。 不接受任何参数:[] { 函数体 } 接受

    2024年02月14日
    浏览(39)
  • 【C++11】lambda表达式 包装器

    在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法: 如果待排序元素为自定义类型,需要用户定义排序时的比较规则: 如果仿函数命名比较规范的话,像上面的命名方式的话那还好,如果遇到了像cmp1 cmp2 cmp3…这种命名方式而且还没有注释的话可以

    2024年02月12日
    浏览(43)
  • 【C++】C++11——lambda表达式

    我们之前都是通过函数指针、仿函数的方式可以像函数使用的对象,在C++11之后,就有了Lambda表达式 为了实现一个比较算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,看代码的人就遭殃了,非常的烦,这些都非常地不方便

    2024年01月17日
    浏览(52)
  • 【C++】C++11语法 ~ lambda 表达式

    (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是 Scort 目前状态:大三非科班啃C++中 🌍博客主页:张小姐的猫~江湖背景 快上车🚘,握好方向盘跟我有一起打天下嘞! 送给自己的一句鸡汤🤔: 🔥真正的大师永远怀着一颗学徒的心 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏 🎉🎉

    2024年01月20日
    浏览(62)
  • C11新特性之Lambda表达式

    优点:  1.可以定义简短的函数。 2.使用lambda表达式使代码更紧凑,可读性更好。 语法: [] 表示不捕获任何变量 [this] 表示值传递方式捕捉当前的 this 指针  [] 表示引用传递方式捕捉所有父作用域的变量(包括 this ) [var] 表示 引用传递 捕捉变量 var [=] 表示值传递方式捕获所

    2023年04月22日
    浏览(43)
  • 【C++干货铺】C++11新特性——lambda表达式 | 包装器

    ========================================================================= 个人主页点击直达:小白不是程序媛 C++系列专栏:C++干货铺 代码仓库:Gitee ========================================================================= 目录 C++98中的排序 lambda表达式 lambda表达式语法 表达式中的各部分说明 lambda表达式的使

    2024年01月21日
    浏览(45)
  • Java语言----反射、枚举以及lambda表达式

    目录 一.反射 1.1反射的基本情况 1.2反射中最重要的类 1.2.1 Class类 1.2.2Field类 1.2.3Constructor类 1.2.4Method类 1.3反射优缺点  二.枚举 2.1概念 2.2枚举(enum)类方法 2.3枚举的构造 三.Lambda表达式 3.1Lambda介绍 3.2 函数式接口 3.3使用lambda表达式 3.2.1不使用Lambda表达式调用 3.2.2使用Lambda表

    2024年02月08日
    浏览(48)
  • 【C++进阶】C++11(下)可变参数模板&lambda表达式&包装器

    我们紧接着上一节的讲解来进行 C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧

    2024年04月11日
    浏览(90)
  • 【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式

    在C++11之前,我们只有引用的概念,没有接触到所谓的左值引用或者是右值引用这种概念,从C++11开始,增加了右值引用的概念,那么现在我们将对引用进行一个概念上的区分。在此之前我们所说的引用都是左值引用,对于左值引用相关的内容,可以去看一看博主之前写的文章

    2024年02月15日
    浏览(57)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包