MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ Lambda 表达式

2022-08-221.2k 阅读

什么是 C++ Lambda 表达式

在 C++ 中,Lambda 表达式是一种匿名函数,它允许你在代码中直接定义一个小型的、内联的函数对象。Lambda 表达式在 C++11 标准中被引入,极大地增强了 C++ 的编程灵活性,特别是在处理算法和函数式编程方面。

从语法结构上看,Lambda 表达式具有以下一般形式:

[capture list](parameter list) -> return type { function body }
  • 捕获列表(capture list):它定义了在 Lambda 表达式内部可以访问的外部变量。捕获列表可以为空,也可以包含一个或多个外部变量。
  • 参数列表(parameter list):与普通函数一样,Lambda 表达式可以接受零个或多个参数。
  • 返回类型(return type):Lambda 表达式的返回类型。在很多情况下,编译器可以自动推导返回类型,所以这部分可以省略。
  • 函数体(function body):包含了实际执行的代码。

例如,一个简单的 Lambda 表达式,用于计算两个整数的和:

auto sum = [](int a, int b) { return a + b; };
int result = sum(3, 5); // result 的值为 8

在这个例子中,[] 表示捕获列表为空,因为 Lambda 表达式不依赖外部变量。(int a, int b) 是参数列表,接受两个整数参数。函数体 { return a + b; } 执行加法操作并返回结果。auto 关键字用于自动推导 sum 的类型,它实际上是一个可调用对象(function object)。

Lambda 表达式的捕获列表

捕获列表是 Lambda 表达式的一个重要部分,它决定了 Lambda 表达式如何访问外部作用域的变量。捕获列表有几种不同的形式:

值捕获

值捕获是指 Lambda 表达式通过复制的方式获取外部变量的值。例如:

int x = 10;
auto lambda = [x]() { return x * 2; };
x = 20;
int result = lambda(); // result 的值为 20,因为捕获的是 x 的值 10

在这个例子中,[x] 表示以值的方式捕获变量 x。当 Lambda 表达式被定义时,它复制了 x 的值。之后对 x 的修改不会影响 Lambda 表达式内部捕获的值。

引用捕获

引用捕获允许 Lambda 表达式访问外部变量的引用,这样对外部变量的修改会反映在 Lambda 表达式内部,反之亦然。例如:

int y = 10;
auto lambda_ref = [&y]() { y = y * 2; return y; };
int result_ref = lambda_ref(); // result_ref 的值为 20,y 的值也变为 20

这里 [&y] 表示以引用的方式捕获变量 y。Lambda 表达式内部对 y 的修改直接影响到外部的 y 变量。

混合捕获

Lambda 表达式也支持同时使用值捕获和引用捕获。例如:

int a = 5;
int b = 10;
auto mixed_capture = [a, &b]() { b = a + b; return b; };
int result_mixed = mixed_capture(); // result_mixed 的值为 15,b 的值也变为 15

在这个例子中,a 是以值的方式捕获,b 是以引用的方式捕获。

隐式捕获

除了显式指定捕获列表,C++ 还支持隐式捕获。隐式捕获分为隐式值捕获([=])和隐式引用捕获([&])。

隐式值捕获 [=] 会自动以值的方式捕获所有在 Lambda 表达式中使用到的外部变量。例如:

int m = 3;
int n = 5;
auto implicit_value_capture = [=]() { return m + n; };
int result_implicit_value = implicit_value_capture(); // result_implicit_value 的值为 8

隐式引用捕获 [&] 会自动以引用的方式捕获所有在 Lambda 表达式中使用到的外部变量。例如:

int p = 7;
int q = 9;
auto implicit_ref_capture = [&]() { p = p + q; return p; };
int result_implicit_ref = implicit_ref_capture(); // result_implicit_ref 的值为 16,p 的值也变为 16

Lambda 表达式的参数列表和返回类型

Lambda 表达式的参数列表和普通函数的参数列表非常相似,它定义了函数接受的参数。例如:

auto multiply = [](int a, int b) { return a * b; };
int product = multiply(4, 6); // product 的值为 24

在这个例子中,(int a, int b) 就是参数列表,接受两个整数参数 ab

关于返回类型,在大多数情况下,编译器可以自动推导 Lambda 表达式的返回类型。例如上述 multiply 的例子,编译器可以根据 return a * b 推导出返回类型为 int

然而,在一些复杂的情况下,例如函数体中有多个 return 语句且返回类型不同,或者需要明确指定返回类型时,就需要显式声明返回类型。例如:

auto conditional_return = [](bool condition) -> int {
    if (condition) {
        return 1;
    } else {
        return 0;
    }
};
int value = conditional_return(true); // value 的值为 1

在这个例子中,由于函数体中有两个不同的 return 语句,为了避免编译器推导错误,显式声明了返回类型为 int

Lambda 表达式在标准库算法中的应用

C++ 的标准库算法,如 std::for_eachstd::find_ifstd::sort 等,经常使用可调用对象作为参数来定制算法的行为。Lambda 表达式为这些算法提供了一种简洁高效的方式来定义定制行为。

std::for_each 与 Lambda 表达式

std::for_each 算法对指定范围内的每个元素应用一个可调用对象。例如,假设有一个整数向量,我们想要将每个元素翻倍:

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::for_each(numbers.begin(), numbers.end(), [](int& num) { num *= 2; });
    for (int num : numbers) {
        std::cout << num << " ";
    }
    return 0;
}

在这个例子中,[](int& num) { num *= 2; } 是一个 Lambda 表达式,它以引用的方式接受一个整数参数 num,并将其翻倍。std::for_each 算法会对 numbers 向量中的每个元素应用这个 Lambda 表达式。

std::find_if 与 Lambda 表达式

std::find_if 算法在指定范围内查找满足特定条件的第一个元素。例如,在一个字符串向量中查找长度大于 5 的字符串:

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

int main() {
    std::vector<std::string> words = {"apple", "banana", "cherry", "date"};
    auto it = std::find_if(words.begin(), words.end(), [](const std::string& word) { return word.length() > 5; });
    if (it != words.end()) {
        std::cout << "Found: " << *it << std::endl;
    } else {
        std::cout << "Not found" << std::endl;
    }
    return 0;
}

这里的 Lambda 表达式 [](const std::string& word) { return word.length() > 5; } 定义了查找条件,即字符串长度大于 5。std::find_if 会在 words 向量中查找满足这个条件的第一个字符串。

std::sort 与 Lambda 表达式

std::sort 算法用于对指定范围内的元素进行排序。我们可以使用 Lambda 表达式来定义自定义的排序规则。例如,对一个整数向量按照绝对值大小进行排序:

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

int main() {
    std::vector<int> values = {3, -5, 2, -7, 1};
    std::sort(values.begin(), values.end(), [](int a, int b) {
        return std::abs(a) < std::abs(b);
    });
    for (int val : values) {
        std::cout << val << " ";
    }
    return 0;
}

在这个例子中,Lambda 表达式 [](int a, int b) { return std::abs(a) < std::abs(b); } 定义了排序规则,即按照绝对值从小到大排序。std::sort 算法会根据这个规则对 values 向量进行排序。

Lambda 表达式与函数对象的比较

传统的 C++ 中,我们可以通过定义函数对象(functor)来实现类似于 Lambda 表达式的功能。函数对象是一个重载了 () 运算符的类。例如,实现一个计算平方的函数对象:

class SquareFunctor {
public:
    int operator()(int num) const {
        return num * num;
    }
};

使用这个函数对象:

SquareFunctor square_functor;
int result_functor = square_functor(5); // result_functor 的值为 25

与 Lambda 表达式相比:

  • 语法简洁性:Lambda 表达式的语法更加简洁直观,不需要定义一个完整的类。例如,实现同样的平方计算,Lambda 表达式只需要 auto square_lambda = [](int num) { return num * num; };,代码量明显更少。
  • 作用域和捕获:Lambda 表达式可以方便地捕获外部变量,而函数对象要实现类似功能需要在类中定义成员变量,并在构造函数中初始化。例如,如果要捕获一个外部变量 factor 用于平方计算后乘以 factor,Lambda 表达式可以写成 int factor = 2; auto square_with_factor = [factor](int num) { return num * num * factor; };,而函数对象则需要在类中添加 factor 成员变量,并在构造函数中初始化。
  • 类型推导:使用 Lambda 表达式时,编译器可以自动推导其类型,而函数对象需要显式定义类型。例如,auto square_lambda = [](int num) { return num * num; };square_lambda 的类型由编译器自动推导,而函数对象 SquareFunctor 则是显式定义的类型。

然而,函数对象也有其优势,比如可以有更复杂的状态和行为,并且在一些旧版本的 C++ 中,函数对象是实现可调用对象的唯一方式。

Lambda 表达式的底层实现

从编译器的角度来看,Lambda 表达式实际上被转换为一个匿名的函数对象(闭包)。当你定义一个 Lambda 表达式时,编译器会生成一个匿名类,这个类重载了 () 运算符。捕获列表中的变量会成为这个类的成员变量,根据捕获方式(值捕获或引用捕获),这些成员变量会以相应的方式初始化。

例如,对于下面的 Lambda 表达式:

int x = 10;
auto lambda = [x]() { return x * 2; };

编译器可能会生成类似这样的匿名类:

class __lambda_1_10 {
public:
    __lambda_1_10(int x) : x_(x) {}
    int operator()() const {
        return x_ * 2;
    }
private:
    int x_;
};

然后 auto lambda = [x]() { return x * 2; }; 实际上相当于 __lambda_1_10 lambda(x);

这种底层实现方式解释了为什么 Lambda 表达式可以像函数对象一样被调用,并且能够捕获外部变量。同时,也说明了为什么 Lambda 表达式在性能上与函数对象相当,因为它们本质上都是通过函数对象来实现的。

Lambda 表达式的高级应用

与 std::function 的结合使用

std::function 是 C++ 标准库提供的一个通用的可调用对象包装器。它可以存储、复制和调用各种可调用对象,包括函数指针、函数对象和 Lambda 表达式。

例如,我们可以定义一个 std::function 类型的变量来存储 Lambda 表达式:

#include <iostream>
#include <functional>

int main() {
    std::function<int(int)> square;
    square = [](int num) { return num * num; };
    int result = square(3); // result 的值为 9
    return 0;
}

在这个例子中,std::function<int(int)> 表示一个接受一个整数参数并返回一个整数的可调用对象。square 变量可以存储符合这个签名的 Lambda 表达式。

std::function 的优势在于它提供了一种统一的方式来处理不同类型的可调用对象。例如,我们可以定义一个函数,它接受一个 std::function 类型的参数,这样这个函数就可以接受不同形式的可调用对象,包括 Lambda 表达式:

void apply_function(std::function<int(int)> func, int num) {
    int result = func(num);
    std::cout << "Result: " << result << std::endl;
}

int main() {
    auto square = [](int num) { return num * num; };
    apply_function(square, 4); // 输出 Result: 16
    return 0;
}

捕获 this 指针

在类的成员函数中,Lambda 表达式可以捕获 this 指针,从而访问类的成员变量和成员函数。例如:

class MyClass {
public:
    int data;
    MyClass(int value) : data(value) {}
    void process() {
        auto lambda = [this]() {
            data = data * 2;
            return data;
        };
        int result = lambda();
        std::cout << "Result: " << result << std::endl;
    }
};

int main() {
    MyClass obj(5);
    obj.process(); // 输出 Result: 10
    return 0;
}

在这个例子中,[this] 表示捕获 this 指针,这样 Lambda 表达式就可以访问 MyClass 类的成员变量 data 并对其进行操作。

可变 Lambda 表达式

默认情况下,Lambda 表达式的函数调用运算符是 const 的,这意味着它不能修改通过值捕获的变量。但是,如果我们需要在 Lambda 表达式内部修改值捕获的变量,可以使用 mutable 关键字。例如:

int count = 0;
auto increment = [count]() mutable {
    count++;
    return count;
};
int result1 = increment(); // result1 的值为 1
int result2 = increment(); // result2 的值为 2

在这个例子中,mutable 关键字使得 Lambda 表达式的函数调用运算符不再是 const,从而可以修改值捕获的变量 count

Lambda 表达式在并发编程中的应用

在 C++ 的并发编程中,Lambda 表达式也有广泛的应用。例如,在使用 std::thread 创建线程时,可以将 Lambda 表达式作为线程执行的函数。

#include <iostream>
#include <thread>

int main() {
    auto task = []() {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread: " << i << std::endl;
        }
    };
    std::thread t(task);
    t.join();
    return 0;
}

在这个例子中,[]() { for (int i = 0; i < 5; ++i) { std::cout << "Thread: " << i << std::endl; } } 是一个 Lambda 表达式,它定义了线程要执行的任务。通过 std::thread t(task); 创建了一个新线程,并将这个 Lambda 表达式作为线程函数。

另外,在使用 std::async 进行异步任务时,也可以使用 Lambda 表达式。std::async 会启动一个异步任务,并返回一个 std::future 对象,通过这个对象可以获取异步任务的结果。例如:

#include <iostream>
#include <future>

int main() {
    auto result_future = std::async([]() {
        int sum = 0;
        for (int i = 1; i <= 100; ++i) {
            sum += i;
        }
        return sum;
    });
    int sum_result = result_future.get();
    std::cout << "Sum: " << sum_result << std::endl;
    return 0;
}

在这个例子中,[]() { int sum = 0; for (int i = 1; i <= 100; ++i) { sum += i; } return sum; } 是一个 Lambda 表达式,它定义了异步任务的逻辑,即计算 1 到 100 的和。std::async 启动这个异步任务,并返回一个 std::future 对象,通过 result_future.get() 获取异步任务的计算结果。

Lambda 表达式的注意事项

捕获列表的生命周期

当使用引用捕获时,需要注意被捕获变量的生命周期。如果被捕获的变量在 Lambda 表达式执行之前就已经销毁,那么会导致未定义行为。例如:

{
    int temp = 10;
    auto lambda = [&temp]() { return temp * 2; };
} // temp 在这里销毁
// lambda(); // 这会导致未定义行为,因为 temp 已经不存在

为了避免这种情况,要么确保被捕获变量的生命周期足够长,要么考虑使用值捕获。

性能问题

虽然 Lambda 表达式在大多数情况下性能表现良好,但在一些极端情况下,特别是在频繁调用且 Lambda 表达式体较大时,可能会产生一定的性能开销。这是因为每次调用 Lambda 表达式时,都需要执行函数调用的开销。在这种情况下,可以考虑将 Lambda 表达式替换为内联函数,以减少函数调用的开销。

类型兼容性

当将 Lambda 表达式作为参数传递给函数,或者存储在 std::function 等容器中时,需要确保 Lambda 表达式的类型与函数参数或容器所期望的类型兼容。这包括参数列表、返回类型以及捕获列表等方面。例如,如果一个函数期望一个接受两个整数并返回一个整数的可调用对象,那么传递的 Lambda 表达式必须满足这个签名。

通过深入理解和掌握 C++ Lambda 表达式的各个方面,开发者可以更加灵活高效地编写代码,充分发挥 C++ 的强大功能,无论是在算法实现、函数式编程还是并发编程等领域。