当前位置:   article > 正文

突破编程_C++_C++11新特性(lambda表达式的实战应用)_c++ lamda函数生命周期

c++ lamda函数生命周期

1 Lambda 表达式的调用与操作

1.1 Lambda 表达式作为函数参数传递

Lambda 表达式可以像普通函数或函数对象一样被传递作为函数的参数。这种灵活性使得 Lambda 表达式在 C++ 的算法库和函数式编程风格中特别有用。当需要将一个小的、匿名的函数作为参数传递给另一个函数时,Lambda 表达式是一个很好的选择。

当 Lambda 表达式作为参数传递时,它会被视为一个函数对象,其类型是一个独特的匿名类,该类重载了调用运算符 operator()。

下面是一个详细的例子,展示了如何将 Lambda 表达式作为参数传递给一个函数:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
// 一个接受函数对象作为参数的函数模板  
template <typename Func>  
void apply_to_elements(std::vector<int>& v, Func func) {  
    for (auto& elem : v) {  
        elem = func(elem);  
    }  
}  
  
int main() 
{  
    std::vector<int> v = {1, 2, 3, 4, 5};  
  
    // 创建一个 Lambda 表达式,它将元素乘以 2  
    auto multiply_by_two = [](int x) { return x * 2; };  
  
    // 将 Lambda 表达式作为参数传递给 apply_to_elements 函数  
    apply_to_elements(v, multiply_by_two);  
  
    // 输出结果  
    for (int x : v) {  
        std::cout << x << ' ';  
    }  
  
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

上面的代码输出为:

2 4 6 8 10
  • 1

这个例子定义了一个模板函数 apply_to_elements,它接受一个 std::vector<int> 和一个函数对象 func 作为参数。函数对象 func 被应用于向量的每一个元素。然后,创建了一个 Lambda 表达式 multiply_by_two,它接受一个整数参数并返回该整数的两倍。最后将这个 Lambda 表达式作为参数传递给 apply_to_elements 函数,以便将向量 v 中的每个元素乘以 2。

Lambda 表达式作为参数传递时,其类型会被自动推导,这样就不需要显式地指定其类型。编译器会根据 Lambda 表达式的捕获子句、参数列表和返回类型来生成一个唯一的匿名类类型。

Lambda 表达式还可以捕获其所在作用域中的变量,这使得它们能够在被传递的函数中使用这些变量。例如:

int threshold = 3;  
auto is_above_threshold = [threshold](int x) { return x > threshold; };  
  
std::vector<int> filtered_v;  
std::copy_if(v.begin(), v.end(), std::back_inserter(filtered_v), is_above_threshold);
  • 1
  • 2
  • 3
  • 4
  • 5

在这个例子中,Lambda 表达式 is_above_threshold 捕获了外部变量 threshold 的值,并用于比较函数。然后,std::copy_if 算法使用这个 Lambda 表达式来决定哪些元素应该被复制到 filtered_v 向量中。

1.2 Lambda 表达式作为函数返回值

在 C++11 中,Lambda 表达式不仅可以作为函数参数传递,还可以作为函数的返回值。这种能力使得 Lambda 表达式在构建高阶函数和实现更复杂的算法时非常有用。高阶函数是指那些接受或返回其他函数的函数。

当 Lambda 表达式作为函数的返回值时,它会被视为一个函数对象,即一个可以像函数一样被调用的对象。Lambda 表达式的类型是一个独特的匿名类,这个类重载了调用运算符 operator(),使得我们可以像调用普通函数一样调用 Lambda 表达式。

下面是一个详细的例子,展示了如何将 Lambda 表达式作为函数的返回值:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
// 一个返回函数对象的函数  
auto create_multiplier(int factor) {  
    return [factor](int x) { return x * factor; };  
}  
  
int main() {  
    // 创建一个将元素乘以 2 的函数对象  
    auto multiply_by_two = create_multiplier(2);  
  
    // 使用返回的 Lambda 表达式(函数对象)  
    std::vector<int> v = {1, 2, 3, 4, 5};  
    for (auto& elem : v) {  
        elem = multiply_by_two(elem);  
    }  
  
    // 输出结果  
    for (int x : v) {  
        std::cout << x << ' ';  
    }  
  
    // 创建一个将元素乘以 3 的函数对象  
    auto multiply_by_three = create_multiplier(3);  
  
    // 重置向量 v  
    v = {1, 2, 3, 4, 5};  
  
    // 使用新的 Lambda 表达式(函数对象)  
    for (auto& elem : v) {  
        elem = multiply_by_three(elem);  
    }  
  
    // 输出结果  
    std::cout << std::endl;  
    for (int x : v) {  
        std::cout << x << ' ';  
    }  
  
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

上面的代码输出为:

2 4 6 8 10
3 6 9 12 15
  • 1
  • 2

在这个例子中,create_multiplier 函数接受一个整数 factor 作为参数,并返回一个 Lambda 表达式,该表达式接受另一个整数 x 并返回 x 与 factor 的乘积。这个 Lambda 表达式作为 create_multiplier 函数的返回值,实际上返回了一个函数对象。

然后,在 main 函数中调用 create_multiplier 来创建两个函数对象:multiply_by_two 和 multiply_by_three。这两个函数对象分别用于将向量 v 中的元素乘以 2 和 3。

值得注意的是,由于 Lambda 表达式的类型是唯一的匿名类,所以不能直接指定其类型作为返回类型。因此,在 create_multiplier 函数的声明中,使用了 auto 关键字来让编译器自动推导返回类型。编译器会根据 Lambda 表达式的捕获子句、参数列表和返回类型来生成一个唯一的匿名类类型,并将其作为 create_multiplier 函数的返回类型。

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

Lambda 表达式在 C++ STL(标准模板库)算法中的应用非常广泛,它们提供了一种简洁、直观的方式来定义临时的函数行为,使得程序员能够更灵活、更直观地编写代码。Lambda 表达式在 STL 算法中常用于定义排序准则、执行特定操作等。

首先,我们需要理解 Lambda 表达式的基本结构。一个 Lambda 表达式通常包括捕获子句、参数列表、返回类型(可选)和函数体。捕获子句决定了哪些变量可以从包含 Lambda 的作用域中捕获;参数列表则定义了Lambda 函数的参数;函数体则是 Lambda 函数执行的具体操作。

在 STL 算法中,Lambda 表达式常常作为谓词(predicate)或函数对象(functor)使用。谓词是一个返回 bool 值的函数或函数对象,用于在算法中定义条件或比较操作。例如,在 std::find_if 算法中,可以使用 Lambda 表达式作为搜索条件来查找容器中满足特定条件的元素。

下面是一个使用Lambda表达式在std::find_if算法中查找向量中大于3的元素的例子:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() 
{  
    std::vector<int> vec = {3, 1, 4, 1, 5, 9};  
    auto it = std::find_if(vec.begin(), vec.end(), [](int x) { return x > 3; });  
    if (it != vec.end()) {  
        std::cout << "Found an element greater than 3: " << *it << std::endl;  
    } else {  
        std::cout << "No element greater than 3 found." << std::endl;  
    }  
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

上面的代码输出为:

Found an element greater than 3: 4
  • 1

在这个例子中,Lambda 表达式 [](int x) { return x > 3; }定义了一个匿名函数,它接受一个整数参数 x 并返回一个布尔值,表示 x 是否大于 3。这个 Lambda 表达式被传递给 std::find_if 算法作为搜索条件。算法遍历向量 vec 中的元素,直到找到第一个满足 Lambda 表达式条件的元素为止。

除了 std::find_if,Lambda 表达式还可以用于其他 STL 算法,如 std::remove_if、std::count_if 等。这些算法接受谓词作为参数,用于定义元素的移除条件或计数条件。通过 Lambda 表达式,可以轻松地为这些算法提供自定义的条件函数。

此外,Lambda 表达式还可以用于定义 STL 排序算法(如std::sort)中的比较函数。例如,可以使用 Lambda 表达式来定义自定义的排序准则,以便按照特定的顺序对容器中的元素进行排序。

2 Lambda 表达式的闭包特性

2.1 闭包的概念与实现

在 C++ 中,闭包(Closure)是一个可以记住并访问其词法作用域(lexical scope)的变量(即使在其函数体之外)的函数对象或 Lambda 表达式。闭包通常用于封装与某个特定上下文相关的代码块,并且可以作为一个值传递和存储。

Lambda 表达式一种实现闭包的方式。Lambda 表达式可以捕获其所在作用域中的变量,并在其函数体中使用这些变量。这使得 Lambda 表达式能够在不同的上下文中保持对外部变量的引用,从而形成一个闭包。

下面是一个简单的 Lambda 表达式闭包的例子:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() 
{  
    int x = 10;  
      
    // 创建一个Lambda表达式闭包,捕获外部变量x  
    auto print_x_times = [x](int n) {  
        for (int i = 0; i < n; ++i) {  
            std::cout << x << ' ';  
        }  
        std::cout << std::endl;  
    };  
      
    // 使用闭包  
    print_x_times(5); // 输出:10 10 10 10 10  
      
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这个例子中,Lambda 表达式 [x](int n) {…}捕获了外部变量 x,并在其函数体中使用了这个变量。由于 Lambda 表达式捕获了 x,它形成了一个闭包,可以在其定义的作用域之外被调用,并且仍然能够访问 x 的值。

2.2 Lambda 表达式的生命周期与闭包的关系

Lambda 表达式的生命周期与闭包的关系是 C++ 中一个非常核心且有趣的概念。这两者紧密相连,对于理解 Lambda 表达式在 C++ 中的行为和用法至关重要。

首先,再回顾一下 Lambda 表达式和闭包的基本概念。Lambda 表达式允许定义一个匿名函数对象,即 Lambda 函数。而闭包则是一个可以记住并访问其词法作用域(lexical scope)的变量的函数对象或 Lambda 表达式。简而言之,闭包是 Lambda 表达式的一个核心特性,它使得 Lambda 表达式能够捕获并保留其定义时的环境信息。

接下来,再探讨一下 Lambda 表达式的生命周期。Lambda 表达式在创建时会被分配内存,并存储相关的捕获变量和函数体等信息。这个 Lambda 表达式对象可以在其创建的作用域内被传递和使用,就像任何其他对象一样。当 Lambda 表达式对象不再被引用时,其生命周期结束,相关的内存会被释放。

然而,Lambda 表达式的生命周期并不仅仅与其自身相关,还与其捕获的变量有着密切的关系。这正是闭包发挥作用的地方。当 Lambda 表达式按值捕获外部变量时,它会创建这些变量的副本,并在其生命周期内持有这些副本。这意味着即使外部变量在 Lambda 表达式被创建之后发生了变化,Lambda 表达式内部使用的仍然是捕获时的值。另一方面,如果 Lambda 表达式按引用捕获外部变量,则它会持有对这些变量的引用,而不是副本。这意味着 Lambda 表达式内部可以直接访问和修改这些外部变量的值,但这些变量的生命周期必须至少与 Lambda 表达式一样长,否则会出现悬挂引用的问题。

因此,Lambda 表达式的生命周期与闭包的关系主要体现在以下几个方面:

  • Lambda 表达式的生命周期决定了其内部捕获的变量的有效期。只有当 Lambda 表达式对象存在时,其捕获的变量才是有效的。
  • 闭包使得 Lambda 表达式能够记住其定义时的环境信息,包括捕获的变量。这使得Lambda表达式能够在不同的上下文中保持对外部变量的引用或值。
  • Lambda 表达式的捕获方式(按值或按引用)会影响其内部对捕获变量的访问方式和变量的生命周期要求。按值捕获会创建变量副本,而按引用捕获会持有变量引用,要求外部变量至少与 Lambda 表达式一样长命。

综上所述,Lambda 表达式的生命周期与闭包紧密相关。闭包使得 Lambda 表达式能够捕获并保留其定义时的环境信息,而 Lambda 表达式的生命周期则决定了其内部捕获的变量的有效期和访问方式。在使用 Lambda 表达式时,我们需要仔细考虑其捕获方式和外部变量的生命周期,以避免潜在的问题和错误。

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

Lambda 表达式与并发编程结合使用,能够大大简化多线程编程的复杂性。C++11 引入了 <thread> 和 <future> 等头文件,为并发编程提供了原生的支持,而 Lambda 表达式则提供了简洁的函数对象定义方式,两者结合使得编写并发代码更加直观和方便。

(1)Lambda 表达式与 std::thread

std::thread 是 C++11 引入的一个类,用于表示和管理线程。可以使用 Lambda 表达式作为 std::thread 的构造参数,从而在新线程中执行 Lambda 表达式定义的任务。

示例:

#include <iostream>  
#include <thread>  
#include <chrono>  
  
int main() {  
    // 使用 Lambda 表达式创建线程  
    std::thread t([&]() {  
        std::this_thread::sleep_for(std::chrono::seconds(1));  
        std::cout << "Hello from thread!\n";  
    });  
  
    // 等待线程完成  
    t.join();  
  
    std::cout << "Main thread continues...\n";  
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

上面的代码输出为:

Hello from thread!
Main thread continues...
  • 1
  • 2

这个例子创建了一个新线程 t,并传递了一个 Lambda 表达式作为参数。Lambda 表达式捕获了外部作用域的所有变量(通过 [&]),并在新线程中休眠一秒后输出一条消息。主线程通过调用 t.join() 等待新线程完成。

(2)Lambda 表达式与 std::async

std::async 是 C++11 中另一个用于并发编程的工具,它返回一个 std::future 对象,该对象表示一个异步操作的结果。你可以使用 Lambda 表达式作为 std::async 的参数,从而异步执行某个任务。

示例:

#include <iostream>  
#include <future>  
#include <chrono>  
  
int main() 
{  
    // 使用 Lambda 表达式异步执行任务  
    std::future<void> fut = std::async(std::launch::async, [&]() {  
        std::this_thread::sleep_for(std::chrono::seconds(1));  
        std::cout << "Task completed asynchronously!\n";  
    });  
  
    // 主线程可以继续执行其他任务...  
    std::cout << "Main thread doing other work...\n";  
    std::this_thread::sleep_for(std::chrono::seconds(2));  
  
    // 获取异步操作的结果(如果有的话)  
    fut.get(); // 这会阻塞,直到异步操作完成  
  
    std::cout << "Main thread continues after asynchronous task...\n";  
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

上面的代码输出为:

Main thread doing other work...
Task completed asynchronously!
Main thread continues after asynchronous task...
  • 1
  • 2
  • 3

这个例子使用 std::async 异步执行一个 Lambda 表达式。Lambda 表达式同样休眠一秒后输出一条消息。主线程继续执行其他任务,并通过调用 fut.get() 等待异步任务完成。注意,std::async 的第一个参数 std::launch::async 表示希望异步执行任务,而不是简单地在当前线程中同步执行。

(3)注意事项

  • 在使用 Lambda 表达式进行并发编程时,需要特别注意数据竞争和线程安全的问题。确保共享数据被正确同步,避免竞态条件。
  • 当 Lambda 表达式捕获外部变量时,要仔细考虑捕获方式(值捕获 = 或引用捕获 &)。值捕获会复制变量的值,而引用捕获则会在 Lambda 表达式中直接使用变量的引用。在并发上下文中,引用捕获可能导致悬挂引用或数据竞争,除非你能确保引用的对象在 Lambda 执行期间一直有效。
  • 使用 std::thread 和 std::async 时,要确保正确管理线程的生命周期,避免线程泄露或过早销毁线程对象。

3 Lambda 表达式在模板编程中的应用

在 C++11 中,Lambda 表达式本身并不是模板类型参数的一部分,因为 Lambda 表达式在编译时生成一个唯一的匿名类类型。然而,我们可以将 Lambda 表达式作为模板函数的参数,这个模板函数可以接受任意可调用的对象,包括 Lambda 表达式。

通过使用 std::function,我们可以将 Lambda 表达式作为模板函数的参数,因为 std::function 可以存储、复制和调用任何可调用的目标,包括函数、函数对象以及 Lambda 表达式。这样,Lambda 表达式就可以在模板函数的上下文中使用了。

下面是一个示例,展示了如何在模板函数中使用 std::function 来接受 Lambda 表达式作为参数:

#include <iostream>  
#include <vector>  
#include <functional> // for std::function  

// 一个模板函数,接受一个容器和一个 std::function 对象作为参数  
template <typename Container, typename Func>
void processContainer(Container& c, Func func) {
	for (const auto& item : c) {
		func(item); // 调用传入的函数对象  
	}
}

int main() {
	std::vector<int> numbers = { 1, 2, 3, 4, 5 };

	// 创建一个 Lambda 表达式,并传递给模板函数  
	processContainer(numbers, [](int n) {
		std::cout << n * 2 << ' '; // 输出每个元素的两倍  
		});

	std::cout << std::endl;

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

上面的代码输出为:

2 4 6 8 10
  • 1

在这个例子中,processContainer 是一个模板函数,它接受一个容器和一个 std::function 对象作为参数。std::function 对象在这里被用作一个可调用对象的包装器,它可以是函数、函数对象或者 Lambda 表达式。在 main 函数中,我们创建了一个 Lambda 表达式,并将其作为第二个参数传递给 processContainer。Lambda 表达式在模板函数中被调用,并输出容器中每个元素的两倍。

虽然 Lambda 表达式本身不是模板类型参数,但是通过使用 std::function,可以在模板函数中接受 Lambda 表达式作为参数,并在运行时调用它们。这支持编写更加通用和灵活的代码,因为 Lambda 表达式提供了定义匿名函数对象的简洁方式。

值得注意的是,使用 std::function 可能会引入一些运行时开销,因为它是一个类型擦除的包装器,这意味着它在内部可能使用了虚函数或其他机制来实现多态性。在性能敏感的代码中,可能需要考虑使用其他方法,比如模板特化和显式函数对象,来避免这种开销。然而,在大多数应用中,std::function 提供的灵活性和易用性通常是可以接受的。

4 Lambda 表达式中的异常处理

Lambda 表达式本质上是一个匿名函数对象,因此它也可以像普通函数一样抛出异常。当 Lambda 表达式内部执行时,如果发生了任何异常,该异常将按照正常的异常处理机制传播到调用者。

Lambda 表达式中的异常处理主要涉及到两个方面:

Lambda 表达式内部抛出异常:如果 Lambda 表达式的主体中有代码抛出了异常,并且该异常没有被 Lambda 表达式内部的任何 catch 块捕获,那么该异常会传播到调用 Lambda 表达式的上下文。

调用 Lambda 表达式的上下文处理异常:调用 Lambda 表达式的代码应该准备好处理由 Lambda 表达式抛出的任何异常。这通常意味着使用 try/catch 块来捕获并处理这些异常。

下面是一个简单的示例,展示了 Lambda 表达式中的异常处理:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() 
{  
    std::vector<int> numbers = {1, 2, 3, 4, 5};  
  
    // 使用 Lambda 表达式作为算法参数  
    try {  
        std::for_each(numbers.begin(), numbers.end(), [](int n) {  
            if (n % 2 == 0) {  
                // 假设这是一个可能会失败的操作  
                throw std::runtime_error("Even number encountered");  
            }  
            std::cout << n << ' ';  
        });  
    } catch (const std::runtime_error& e) {  
        // 捕获并处理异常  
        std::cerr << "Caught an exception: " << e.what() << std::endl;  
    }  
  
    return 0;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

上面的代码输出为:

1 Caught an exception: Even number encountered
  • 1

这个例子定义了一个 Lambda 表达式,它遍历 numbers 向量中的每个元素。如果元素是偶数,Lambda 表达式会抛出一个 std::runtime_error 异常。我们在调用 std::for_each 算法时使用了 try/catch 块来捕获这个异常。

当执行到偶数时(在这个例子中是 2 和 4),Lambda 表达式会抛出异常,该异常被外部的 catch 块捕获,并打印出错误消息。

需要注意以下几点:

  • Lambda 表达式内部抛出的异常类型可以是任何可以从 Lambda 表达式中抛出的类型。
  • 异常处理的责任在于调用 Lambda 表达式的代码。Lambda 表达式本身不会处理其抛出的异常。
  • 如果 Lambda 表达式被用作一个异步操作(比如通过线程或异步任务队列),那么异常处理将变得更加复杂,因为异常不会直接传播到调用线程。在这种情况下,可能需要使用特殊的机制(如异常指针或错误码)来传递和处理异常。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/602486
推荐阅读
相关标签
  

闽ICP备14008679号