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

C++ Lambda 表达式在 STL 中的妙用

2022-12-046.6k 阅读

C++ Lambda 表达式基础回顾

在深入探讨 C++ Lambda 表达式在 STL 中的妙用之前,我们先来回顾一下 Lambda 表达式的基础概念。Lambda 表达式本质上是一种匿名函数,它允许我们在代码中定义并使用一个短小的、内联的函数对象,而无需为其显式地命名。其基本语法如下:

[capture list](parameter list) -> return type {
    function body
}
  • 捕获列表(capture list):用于指定在 Lambda 表达式内部可以访问的外部变量。捕获列表可以为空,也可以包含一个或多个变量。例如,[&] 表示以引用方式捕获所有外部变量,[=] 表示以值方式捕获所有外部变量。如果只想捕获特定变量,如 [a] 表示以值方式捕获变量 a[&a] 表示以引用方式捕获变量 a
  • 参数列表(parameter list):与普通函数的参数列表类似,定义了 Lambda 表达式接受的参数。可以为空,例如 () -> void { /*... */ }
  • 返回类型(return type):指定 Lambda 表达式的返回值类型。如果 Lambda 表达式的函数体只有一条 return 语句,C++ 编译器可以自动推断返回类型,此时可以省略 -> return type 部分。例如,[]{ return 42; },编译器可以推断出返回类型为 int
  • 函数体(function body):包含实际执行的代码逻辑。

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

auto add = [](int a, int b) { return a + b; };
int result = add(3, 5);
std::cout << "The result of 3 + 5 is: " << result << std::endl;

在这个例子中,add 是一个 Lambda 表达式对象,它接受两个 int 类型的参数,并返回它们的和。

C++ Lambda 表达式在 STL 算法中的应用

1. std::for_each 与 Lambda 表达式

std::for_each 是 STL 中的一个算法,它对指定范围内的每个元素应用一个给定的函数。在没有 Lambda 表达式之前,我们可能需要定义一个单独的函数或者函数对象来作为 std::for_each 的参数。例如,要打印一个 std::vector<int> 中的所有元素:

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

void printElement(int num) {
    std::cout << num << " ";
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::for_each(numbers.begin(), numbers.end(), printElement);
    std::cout << std::endl;
    return 0;
}

使用 Lambda 表达式后,代码变得更加简洁和紧凑:

#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) { std::cout << num << " "; });
    std::cout << std::endl;
    return 0;
}

这里,Lambda 表达式 [](int num) { std::cout << num << " "; } 作为 std::for_each 的第三个参数,直接定义并使用,避免了单独定义函数的麻烦。

2. std::find_if 与 Lambda 表达式

std::find_if 用于在指定范围内查找满足特定条件的第一个元素。假设我们有一个 std::vector<int>,要查找第一个大于 10 的元素:

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

bool isGreaterThanTen(int num) {
    return num > 10;
}

int main() {
    std::vector<int> numbers = {5, 8, 12, 15, 20};
    auto it = std::find_if(numbers.begin(), numbers.end(), isGreaterThanTen);
    if (it != numbers.end()) {
        std::cout << "The first number greater than 10 is: " << *it << std::endl;
    } else {
        std::cout << "No number greater than 10 found." << std::endl;
    }
    return 0;
}

使用 Lambda 表达式后:

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

int main() {
    std::vector<int> numbers = {5, 8, 12, 15, 20};
    auto it = std::find_if(numbers.begin(), numbers.end(), [](int num) { return num > 10; });
    if (it != numbers.end()) {
        std::cout << "The first number greater than 10 is: " << *it << std::endl;
    } else {
        std::cout << "No number greater than 10 found." << std::endl;
    }
    return 0;
}

Lambda 表达式 [](int num) { return num > 10; } 简洁地定义了查找条件,使得代码更加清晰易读。

3. std::sort 与 Lambda 表达式

std::sort 用于对指定范围内的元素进行排序。默认情况下,它使用 < 运算符进行升序排序。但如果我们想要进行降序排序,或者根据自定义的规则进行排序,就可以使用 Lambda 表达式。例如,对一个 std::vector<int> 进行降序排序:

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

int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9};
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; });
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

这里,Lambda 表达式 [](int a, int b) { return a > b; } 定义了降序排序的规则,即 a 大于 b 时返回 true,表示 a 应该排在 b 前面。

4. std::transform 与 Lambda 表达式

std::transform 可以将一个范围内的元素按照指定的规则进行转换,并将结果存储到另一个范围中。例如,将一个 std::vector<int> 中的每个元素乘以 2,并存储到另一个 std::vector<int> 中:

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

int multiplyByTwo(int num) {
    return num * 2;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> result(numbers.size());
    std::transform(numbers.begin(), numbers.end(), result.begin(), multiplyByTwo);
    for (int num : result) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

使用 Lambda 表达式后:

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

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

Lambda 表达式 [](int num) { return num * 2; } 定义了转换规则,使代码更加简洁。

C++ Lambda 表达式在 STL 容器中的应用

1. std::priority_queue 与 Lambda 表达式

std::priority_queue 是一个优先队列容器,默认情况下,它按照元素的 < 运算符进行降序排列(大顶堆)。如果我们想要创建一个小顶堆,或者根据自定义的比较规则进行排序,可以使用 Lambda 表达式。

#include <iostream>
#include <queue>
#include <vector>

int main() {
    // 使用 Lambda 表达式创建一个小顶堆
    auto compare = [](int a, int b) { return a > b; };
    std::priority_queue<int, std::vector<int>, decltype(compare)> minHeap(compare);
    minHeap.push(5);
    minHeap.push(2);
    minHeap.push(8);
    minHeap.push(1);
    minHeap.push(9);

    while (!minHeap.empty()) {
        std::cout << minHeap.top() << " ";
        minHeap.pop();
    }
    std::cout << std::endl;
    return 0;
}

在这个例子中,Lambda 表达式 [](int a, int b) { return a > b; } 定义了小顶堆的比较规则,即 a 大于 b 时返回 true,表示 a 应该排在 b 后面。

2. std::setstd::map 与 Lambda 表达式

std::setstd::map 是有序关联容器,默认情况下,它们使用 < 运算符进行排序。如果我们想要根据自定义的比较规则进行排序,可以在构造函数中传入一个比较函数对象,此时 Lambda 表达式就可以派上用场。

例如,创建一个按照字符串长度从小到大排序的 std::set<std::string>

#include <iostream>
#include <set>
#include <string>

int main() {
    auto compareLength = [](const std::string& a, const std::string& b) { return a.length() < b.length(); };
    std::set<std::string, decltype(compareLength)> stringSet(compareLength);
    stringSet.insert("apple");
    stringSet.insert("banana");
    stringSet.insert("cherry");
    stringSet.insert("date");

    for (const std::string& str : stringSet) {
        std::cout << str << " ";
    }
    std::cout << std::endl;
    return 0;
}

这里,Lambda 表达式 [](const std::string& a, const std::string& b) { return a.length() < b.length(); } 定义了按照字符串长度比较的规则,使得 std::set 中的字符串按照长度从小到大排序。

捕获列表在 STL 应用中的深入理解

1. 值捕获

在 STL 算法和容器中使用 Lambda 表达式时,值捕获是一种常见的捕获方式。例如,我们有一个变量 threshold,要在 std::find_if 中使用它来查找 std::vector<int> 中第一个大于 threshold 的元素:

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

int main() {
    int threshold = 10;
    std::vector<int> numbers = {5, 8, 12, 15, 20};
    auto it = std::find_if(numbers.begin(), numbers.end(), [threshold](int num) { return num > threshold; });
    if (it != numbers.end()) {
        std::cout << "The first number greater than " << threshold << " is: " << *it << std::endl;
    } else {
        std::cout << "No number greater than " << threshold << " found." << std::endl;
    }
    return 0;
}

在这个例子中,Lambda 表达式通过 [threshold] 以值方式捕获了 threshold 变量。这意味着在 Lambda 表达式内部使用的 threshold 是外部 threshold 的一个副本,即使外部的 threshold 发生变化,Lambda 表达式内部的 threshold 值也不会改变。

2. 引用捕获

有时候,我们希望在 Lambda 表达式内部能够修改外部变量的值,或者避免复制大对象带来的性能开销,这时就可以使用引用捕获。例如,在 std::for_each 中统计 std::vector<int> 中奇数的个数:

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

int main() {
    int oddCount = 0;
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::for_each(numbers.begin(), numbers.end(), [&oddCount](int num) { if (num % 2 != 0) oddCount++; });
    std::cout << "The number of odd numbers is: " << oddCount << std::endl;
    return 0;
}

这里,Lambda 表达式通过 [&oddCount] 以引用方式捕获了 oddCount 变量。因此,在 Lambda 表达式内部对 oddCount 的修改会反映到外部变量上。

3. 混合捕获

在实际应用中,我们可能会同时需要值捕获和引用捕获。例如,有一个 std::vector<int>,要查找第一个大于某个阈值且小于另一个阈值的元素,同时记录查找的次数:

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

int main() {
    int lowerThreshold = 5;
    int upperThreshold = 15;
    int searchCount = 0;
    std::vector<int> numbers = {3, 8, 12, 18, 20};
    auto it = std::find_if(numbers.begin(), numbers.end(), [lowerThreshold, &searchCount, upperThreshold](int num) {
        searchCount++;
        return num > lowerThreshold && num < upperThreshold;
    });
    if (it != numbers.end()) {
        std::cout << "The first number between " << lowerThreshold << " and " << upperThreshold << " is: " << *it << std::endl;
    } else {
        std::cout << "No number between " << lowerThreshold << " and " << upperThreshold << " found. Search count: " << searchCount << std::endl;
    }
    return 0;
}

在这个例子中,lowerThresholdupperThreshold 以值方式捕获,因为我们不希望在 Lambda 表达式内部修改它们的值;而 searchCount 以引用方式捕获,以便在 Lambda 表达式内部记录查找次数并反映到外部变量。

Lambda 表达式与 STL 的性能考虑

1. 内联与代码膨胀

Lambda 表达式作为内联函数对象,通常会被编译器内联展开。这意味着在调用 Lambda 表达式的地方,编译器会直接将 Lambda 表达式的函数体插入,从而避免了函数调用的开销。例如,在 std::for_each 中使用 Lambda 表达式:

#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) { std::cout << num << " "; });
    std::cout << std::endl;
    return 0;
}

编译器可能会将 Lambda 表达式 [](int num) { std::cout << num << " "; } 内联展开,使得 std::for_each 的循环体直接包含输出语句,从而提高了性能。然而,如果 Lambda 表达式函数体较大,过多的内联可能会导致代码膨胀,增加可执行文件的大小和内存占用。

2. 捕获方式与性能

值捕获会复制捕获的变量,对于大对象来说,这可能会带来较大的性能开销。例如,如果捕获一个大的 std::vector

#include <iostream>
#include <vector>

int main() {
    std::vector<int> largeVector(1000000, 42);
    auto lambda = [largeVector]() {
        // 对 largeVector 进行操作
        return largeVector.size();
    };
    return 0;
}

这里,值捕获 largeVector 会复制整个 std::vector,这在时间和空间上都是昂贵的操作。在这种情况下,引用捕获 [&largeVector] 可能是更好的选择,因为它不会复制对象,只是引用外部的 largeVector。但需要注意的是,引用捕获可能会引入生命周期问题,比如当外部对象在 Lambda 表达式执行之前被销毁,就会导致未定义行为。

3. STL 算法与 Lambda 表达式的协同性能

不同的 STL 算法与 Lambda 表达式的协同性能也有所不同。例如,std::sort 算法在使用 Lambda 表达式作为比较函数时,由于 Lambda 表达式通常会被内联,所以在排序过程中比较操作的开销相对较小。但对于一些需要多次调用 Lambda 表达式的算法,如 std::find_if,如果 Lambda 表达式的执行开销较大,可能会影响整体性能。在这种情况下,可以考虑将复杂的逻辑提取到一个单独的函数中,然后在 Lambda 表达式中调用该函数,以提高代码的可读性和性能。

Lambda 表达式在 STL 多线程场景中的应用

1. 并行算法与 Lambda 表达式

C++ 标准库提供了并行版本的 STL 算法,如 std::for_each_n 的并行版本 std::execution::par_unseq::for_each_n。在并行算法中使用 Lambda 表达式可以充分利用多核处理器的性能。例如,对一个 std::vector<int> 中的每个元素进行平方操作,使用并行算法和 Lambda 表达式:

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

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

这里,std::execution::par_unseq::for_each_n 会并行地对 numbers 中的元素应用 Lambda 表达式 [](int& num) { num = num * num; },从而加快计算速度。

2. 线程安全与 Lambda 表达式

在多线程场景中使用 Lambda 表达式时,需要注意线程安全问题。例如,如果多个线程同时访问和修改被 Lambda 表达式捕获的变量,可能会导致数据竞争。假设我们有一个全局变量 counter,多个线程通过 std::for_each 和 Lambda 表达式来递增 counter

#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void incrementCounter() {
    std::vector<int> numbers(1000);
    std::for_each(numbers.begin(), numbers.end(), [&counter](int) {
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    });
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);
    t1.join();
    t2.join();
    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

在这个例子中,通过 std::mutexstd::lock_guard 来保护对 counter 的访问,确保线程安全。如果不进行适当的同步,counter 的最终值可能是不确定的。

3. 任务并行与 Lambda 表达式

除了并行算法,我们还可以使用 std::asyncstd::future 来实现任务并行,并结合 Lambda 表达式。例如,计算两个独立任务的结果并等待它们完成:

#include <iostream>
#include <future>

int main() {
    auto task1 = std::async([]() {
        // 模拟一些计算
        int result = 0;
        for (int i = 0; i < 1000000; ++i) {
            result += i;
        }
        return result;
    });

    auto task2 = std::async([]() {
        // 模拟另一些计算
        int result = 1;
        for (int i = 1; i <= 10; ++i) {
            result *= i;
        }
        return result;
    });

    int result1 = task1.get();
    int result2 = task2.get();
    std::cout << "Result of task1: " << result1 << ", Result of task2: " << result2 << std::endl;
    return 0;
}

这里,std::async 启动了两个异步任务,每个任务由一个 Lambda 表达式定义。通过 std::futureget 方法等待任务完成并获取结果。

常见问题与解决方法

1. 捕获列表导致的错误

在使用捕获列表时,可能会出现一些错误。例如,捕获一个未定义的变量:

#include <iostream>

int main() {
    // 这里没有定义变量 a
    auto lambda = [a]() { std::cout << a << std::endl; };
    return 0;
}

编译器会报错提示 a 未定义。解决方法是确保捕获列表中的变量在外部已经定义。

另一个常见问题是捕获变量的生命周期问题。如前文所述,引用捕获可能导致外部变量在 Lambda 表达式执行之前被销毁,从而引发未定义行为。例如:

#include <iostream>
#include <functional>

std::function<void()> createLambda() {
    int num = 42;
    return [&num]() { std::cout << num << std::endl; };
}

int main() {
    auto lambda = createLambda();
    // num 在这里已经被销毁
    lambda();
    return 0;
}

在这个例子中,createLambda 函数返回的 Lambda 表达式以引用方式捕获了 num,但当 createLambda 函数返回后,num 被销毁。当调用 lambda 时,就会访问已销毁的变量,导致未定义行为。解决方法是要么以值方式捕获 num,要么确保 num 的生命周期足够长。

2. Lambda 表达式返回类型推断错误

虽然 C++ 编译器通常可以自动推断 Lambda 表达式的返回类型,但在某些复杂情况下可能会出现推断错误。例如:

#include <iostream>

auto createLambda() {
    return [](int a, int b) {
        if (a > b) {
            return a;
        } else {
            return static_cast<double>(b);
        }
    };
}

int main() {
    auto lambda = createLambda();
    auto result = lambda(3, 5);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

在这个例子中,由于 if - else 分支返回不同类型(intdouble),编译器无法正确推断返回类型。解决方法是显式指定返回类型:

#include <iostream>

auto createLambda() {
    return [](int a, int b) -> double {
        if (a > b) {
            return static_cast<double>(a);
        } else {
            return static_cast<double>(b);
        }
    };
}

int main() {
    auto lambda = createLambda();
    auto result = lambda(3, 5);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

3. STL 算法与 Lambda 表达式不兼容问题

有时候,STL 算法可能对 Lambda 表达式的参数和返回类型有特定要求。例如,std::sort 的比较函数必须满足严格弱序关系。如果定义的 Lambda 表达式不满足这个要求,可能会导致未定义行为。假设我们定义了一个不满足严格弱序关系的比较函数:

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

int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9};
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a <= b; });
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

这里的 [](int a, int b) { return a <= b; } 不满足严格弱序关系(因为对于 a == b 时也返回 true),可能会导致 std::sort 行为异常。正确的比较函数应该是 [](int a, int b) { return a < b; }

通过对以上内容的学习,我们深入了解了 C++ Lambda 表达式在 STL 中的各种妙用,包括在 STL 算法和容器中的应用、捕获列表的使用、性能考虑、多线程场景下的应用以及常见问题的解决方法。希望这些知识能帮助读者在实际编程中更灵活、高效地运用 Lambda 表达式与 STL。