【C++11】深入解析 C++ Lambda 表达式:定义、底层实现与应用

一、定义

Lambda表达式是C++11引入的一种函数对象的匿名表示方法。它可以用于定义轻量级的、临时的、内联的函数对象,通常用于函数式编程的场景。


1. 语法结构

Lambda表达式的 基本语法 如下:

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type {
	// 函数体 - statement
}

ambda表达式各部分说明:

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

需要注意的是:

在lambda函数定义中,参数列表返回值类型都是可选部分,而捕捉列表函数体可以为。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情

在这里插入图片描述


2. 关于 捕获列表

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用

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

二、性质

  1. 父作用域指包含lambda函数的语句块
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。例如:

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

  1. 捕捉列表不允许变量重复传递,否则就会导致编译错误。例如:

比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

  1. 在块作用域以外的lambda函数捕捉列表必须为空。
  2. 块作用域中的lambda函数仅能捕捉父作用域中局部变量捕捉任何非此作用域或者非局部变量都会导致编译报错
  3. lambda表达式之间不能相互赋值,即使看起来类型相同

原因:Lambda表达式是一种匿名函数,它生成的函数对象是一个闭包。每个Lambda表达式都有其自己的类型,并且不同的Lambda表达式具有不同的类型。因此,Lambda表达式之间不能直接相互赋值。

可以使用 函数指针 / std::function 进行函数赋值

// 函数指针实现 函数赋值
void f1() {
    std::cout << "hello" << std::endl;
}

void f2() {
    std::cout << "hello" << std::endl;
}

int main()
{
    void (*pf)();
    pf = f1; // 将函数指针pf指向f1
    pf();    // 调用f1函数

    pf = f2; // 将函数指针pf指向f2
    pf();    // 调用f2函数

    return 0;
}

三、常见用法

1. 作为函数对象传递:

Lambda表达式可以作为参数传递给需要函数对象作为参数的函数,例如:

// 函数接受一个函数对象作为参数
void process(std::function<void(int)> func) {
    // ...
}

// 调用时使用Lambda表达式作为参数
process([](int x) {
    // Lambda表达式的逻辑
    // ...
});

2. 在算法中使用:

Lambda表达式可以与标准库中的算法一起使用,用于指定算法的操作行为,例如:

std::vector<int> nums = {1, 2, 3, 4, 5};

// 使用Lambda表达式作为排序准则
std::sort(nums.begin(), nums.end(), [](int a, int b) {
    return a < b;
});

// 使用Lambda表达式作为条件判断
auto it = std::find_if(nums.begin(), nums.end(), [](int num) {
    return num > 3;
});

3. 自定义比较器:

Lambda表达式可以用来定义自定义的比较器,例如在容器排序或二叉搜索树中使用,例如:

struct Person {
    std::string name;
    int age;
};

std::vector<Person> people = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 20}};

// 使用Lambda表达式定义自定义比较器
std::sort(people.begin(), people.end(), [](const Person& p1, const Person& p2) {
    return p1.age < p2.age;
});

4. 异步编程:

Lambda表达式可以用于异步编程,例如与std::async一起使用,创建异步任务,例如:

auto future = std::async([]() {
    // 异步任务的逻辑
    // ...
    return result;
});

// 获取异步任务的结果
auto result = future.get();

四、一些问题

lambda 表达式与仿函数的区别

仿函数介绍

仿函数通常通过类成员或构造函数来保存外部变量的值。仿函数本身并没有直接的语法支持来捕获外部变量。为了实现类似的功能,通常通过类的成员变量或构造函数来传递和存储外部数据。

示例

struct AddX {
    int x;
    AddX(int x_) : x(x_) {}
    int operator()(int a) {
        return a + x;
    }
};

AddX add_x(10);
std::cout << add_x(5); // 输出 15

下表是lambda与仿函数的区别:

特性Lambda表达式仿函数(Function Object)
语法简洁,匿名函数通过类和重载 operator() 实现
捕获外部变量可以直接捕获外部变量通过类成员或构造函数传递外部变量
灵活性适合临时和简单的功能适合复杂的功能,支持继承、状态管理等
内存开销通常较小,但捕获外部变量时可能有额外开销可能稍大,尤其是当类比较复杂时
可扩展性灵活性较差,不能扩展为其他复杂结构(如继承)支持扩展,可以拥有成员函数、虚函数等
可读性与用途简洁且易于理解,适用于局部函数对象的场景

lambda 表达式的底层实现原理

1. Lambda表达式的本质

Lambda 表达式本质上是一个 匿名函数对象,它是由编译器根据表达式自动生成的一个类。这个类实现了 operator(),使得该对象可以像函数一样被调用。Lambda 表达式通常有一个捕获列表([capture]),捕获列表决定了 Lambda 是否可以使用外部作用域中的变量。

2. Lambda 底层实现的主要步骤

2.1 生成匿名类

Lambda 表达式会被编译器转换为一个 匿名类,该类继承自一个无继承关系的基类(如果有捕获,通常会包含额外的成员变量来存储捕获的值)。这个匿名类定义了一个 operator() 方法,该方法就是 Lambda 的函数体。

  • 如果 Lambda 不捕获外部变量(即空捕获列表 []),编译器生成的类只有 operator(),并且它不含有额外的成员。
  • 如果 Lambda 捕获了外部变量(例如 [x, y]),编译器会为每个捕获的变量生成一个成员变量,用于存储捕获的值。

示例:

auto add = [](int a, int b) { return a + b; };

编译器会将上述 Lambda 表达式转换为类似如下的匿名类:

struct AddLambda {
    int operator()(int a, int b) const {
        return a + b;
    }
};
2.2 捕获外部变量

如果 Lambda 捕获了外部变量,编译器会为每个捕获的变量生成一个成员变量。捕获列表决定了 Lambda 如何访问这些变量(按值捕获或按引用捕获)。

  • 按值捕获([x]:编译器会在匿名类中生成一个 x 的副本作为成员变量。
  • 按引用捕获([&x]:编译器会将 x 的引用存储为成员变量。

示例:

int x = 10;
auto add = [x](int a) { return a + x; };

这将被转换成如下的匿名类:

struct AddLambda {
    int x; // 存储捕获的外部变量 x
    AddLambda(int x_) : x(x_) {} // 构造函数,初始化 x

    int operator()(int a) const {
        return a + x;
    }
};
2.3 编译器生成对象

Lambda 会被编译成一个类型为该匿名类的对象。在编译时,Lambda 会根据其捕获方式被转化为该匿名类的一个实例,并且通过该实例来执行相应的操作。

AddLambda add_lambda(x); // 实例化匿名类对象
std::cout << add_lambda(5); // 调用 operator()

3. Lambda 表达式的不同捕获方式

Lambda 的捕获方式直接影响其底层实现,捕获可以分为按值捕获和按引用捕获两种情况,编译器为每种捕获方式生成不同的代码。

3.1 按值捕获(Capture by value)

按值捕获时,编译器会将捕获的变量的副本保存在 Lambda 对象的成员中。这意味着 Lambda 对象中会包含捕获的变量的一个 拷贝

示例:

int x = 10;
auto add = [x](int a) { return a + x; };

这将转换为类似如下的类:

struct AddLambda {
    int x; // 捕获的变量 x
    AddLambda(int x_) : x(x_) {} // 构造函数初始化 x

    int operator()(int a) const {
        return a + x;
    }
};
3.2 按引用捕获(Capture by reference)

按引用捕获时,编译器会将捕获的变量的引用保存到 Lambda 对象中,而不是拷贝值。这意味着 Lambda 可以修改外部变量的值。

示例:

int x = 10;
auto add = [&x](int a) { return a + x; };

这将转换为类似如下的类:

struct AddLambda {
    int& x; // 捕获的变量 x 的引用
    AddLambda(int& x_) : x(x_) {} // 构造函数初始化引用 x

    int operator()(int a) const {
        return a + x;
    }
};
3.3 混合捕获(Capture by value and reference)

你也可以同时按值和按引用捕获外部变量。在这种情况下,编译器会为每个捕获的变量生成相应的成员变量,按值捕获生成副本,按引用捕获生成引用。

示例:

int x = 10, y = 20;
auto add = [x, &y](int a) { return a + x + y; };

这将转换为类似如下的类:

struct AddLambda {
    int x; // 捕获的变量 x(按值)
    int& y; // 捕获的变量 y(按引用)
    
    AddLambda(int x_, int& y_) : x(x_), y(y_) {} // 构造函数初始化

    int operator()(int a) const {
        return a + x + y;
    }
};

4. Lambda 表达式的返回类型

Lambda 表达式的返回类型通常由编译器推导,但也可以显式指定返回类型。返回类型的推导是通过 -> 后面的类型或者编译器的自动推导来完成的。

示例:

auto add = [](int a, int b) -> int { return a + b; };

在底层实现中,编译器会根据表达式的返回值推导出返回类型,并将其用在生成的匿名类的 operator() 的返回类型中。

5. Lambda 表达式的性能优化

编译器通常会对 Lambda 表达式进行优化,包括:

  • 内联优化:如果 Lambda 的大小适中,编译器可能会将其内联,消除函数调用开销。
  • 捕获优化:对于按值捕获的 Lambda,编译器可以在适当的情况下共享捕获的值,减少不必要的内存开销。

6. 使用类型擦除的情况(std::function

当 Lambda 表达式被传递给需要 std::function 或其他通用函数指针类型的 API 时,编译器会使用类型擦除技术将 Lambda 的具体类型转化为一个可以存储不同类型的 std::function。这种情况下,Lambda 会被包装成一个类型擦除的对象,这样它可以作为一个通用的函数对象传递。

7. 总结

Lambda 表达式的底层实现可以总结为以下几个关键点:

  1. 匿名类:Lambda 表达式会被编译器转换为一个匿名类,该类实现了 operator()
  2. 捕获外部变量:捕获的外部变量会被存储为类的成员变量,按值捕获或按引用捕获会影响 Lambda 对象的内存布局。
  3. 返回类型和推导:Lambda 表达式的返回类型可以由编译器自动推导,也可以显式指定。
  4. 类型擦除:在需要时,Lambda 表达式可以被包装为通用的函数对象(如 std::function),以便类型擦除。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值