c++ lambda 表达式简单说明和深入分析

1.概要

 lambda 表达式有啥用?

关于 lambda,其实就是一个小函数,在函数内的函数;

比如我有一个功能,在一个函数中要多次调用,封装成一个函数吧,感觉没有必要,在代码中多次重复还感觉有点冗余,怎么办呢,用lambda 表达式,把他封装起来就行了。

 lambda 表达式怎么用?

函数怎么用,他就怎么用。函数有什么?函数名,入参,返回值;如果是成员函数还可以使用类的成员,这部分在这里叫“捕获列表”,就是捕获这个函数内的变量的列表,相当于这个函数的成员变量,不准备,但就是这个意思;就这4部分就够了,但实际上是6部分,不过另两个不常用,这里先不说。

函数名,入参和普通函数没什么差别,这里说一下“捕获列表”和“返回值

捕获列表这样用

int a

[a]

这就表示,在lambda 表达式 内可以使用 在函数内的这个a变量;

返回值这样用

->int

这就表示返回值是int行

举一个整体的例子

{
    //...某某函数内
    
    //函数内的变量
    int x = 10;  
    //定义 lambda 表达式
    auto multiply = [x](int n) { return n * x; };

    //使用
    std::cout << multiply(n) << " ";

    //....
}

 总结:其实就是在一个函数内声明一个小函数,可以在函数内用

2.主体

1.c++ lambda 表达式

在C++中,lambda表达式是一种匿名函数,它可以内联定义并且通常用于定义简短的回调函数或作为参数传递给算法函数。Lambda表达式在C++11标准中引入,为编写更简洁和灵活的代码提供了便利。

一个完整的lambda表达式包含以下几个部分:

  1. 捕获列表:用于捕获外部变量。可以按值捕获(=)或按引用捕获(&),也可以显式指定捕获哪些变量。
  2. 参数列表:与普通函数的参数列表类似,可以为空。
  3. 可变说明符:可选的mutable关键字,允许在lambda表达式内部修改按值捕获的变量。
  4. 异常说明符:可选的,用于指定lambda可能抛出的异常类型。
  5. 返回类型:可以省略,编译器会自动推导返回类型。如果需要显式指定,可以使用尾置返回类型语法。
  6. lambda体:包含lambda的具体实现。

一个简单的lambda表达式示例如下:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> numbers = {1, 2, 3, 4, 5};  
  
    // Lambda表达式:按值捕获`x`,参数为`n`,返回`n * x`  
    int x = 10;  
    auto multiply = [x](int n) { return n * x; };  
  
    std::for_each(numbers.begin(), numbers.end(), [&](int n) {  
        std::cout << multiply(n) << " ";  
    });  
  
    return 0;  
}

在这个例子中,我们定义了一个lambda表达式multiply,它按值捕获了变量x,并接受一个参数n,返回n * x。然后我们使用std::for_each和另一个lambda表达式来遍历numbers向量,并打印出每个元素经过multiply处理后的结果。

捕获列表的详细说明

  • [ ]:不捕获任何外部变量。
  • [=]:以值捕获方式捕获所有外部变量。
  • [&]:以引用捕获方式捕获所有外部变量。
  • [x]:以值捕获方式捕获变量x
  • [&x]:以引用捕获方式捕获变量x
  • [=, &x]:以值捕获方式捕获所有外部变量,但x以引用捕获。
  • [&, x]:以引用捕获方式捕获所有外部变量,但x以值捕获。

可变说明符

默认情况下,lambda表达式内部不能修改按值捕获的变量。如果需要修改,可以使用mutable关键字:

auto lambda = [x](int n) mutable { x = n; return x; };

在这个例子中,x可以按值捕获,但在lambda体内部可以被修改。

lambda表达式是C++中强大且灵活的特性,通过合理使用可以大大简化代码,提高可读性。

 2.c++ lambda 表达式由6部分构成,举一个这6部分都用到的例子

当然,下面是一个C++ lambda表达式,它包含了所有六个部分:捕获列表、参数列表、可变说明符、异常说明符、返回类型和lambda体。请注意,这样的例子可能在实际编程中并不常见,因为很多情况下我们并不需要显式地指定所有这些部分,但为了展示完整性,我将提供一个包含所有部分的示例。

#include <iostream>  
#include <stdexcept>  
  
int main() {  
    int a = 1;  
    int& b = a;  
  
    // Lambda表达式示例,包含所有六个部分  
    auto lambda = [a, &b](int x, int y) mutable noexcept -> int {  
        a = x; // 修改按值捕获的a  
        b = y; // 修改按引用捕获的b  
        if (x < 0 || y < 0) {  
            throw std::invalid_argument("Negative value not allowed");  
        }  
        return x + y + a + b;  
    };  
  
    try {  
        std::cout << lambda(5, 10) << std::endl; // 输出结果  
    } catch (const std::invalid_argument& e) {  
        std::cerr << "Exception caught: " << e.what() << std::endl;  
    }  
  
    return 0;  
}

在这个例子中:

  1. 捕获列表[a, &b] 捕获了外部变量a的副本和b的引用。
  2. 参数列表(int x, int y) 定义了两个整型参数xy
  3. 可变说明符mutable 允许在lambda体内部修改按值捕获的变量a
  4. 异常说明符noexcept 表示这个lambda不会抛出异常(但注意,如果xy是负数,实际上会抛出std::invalid_argument,这里只是为了展示异常说明符的用法)。在实际应用中,应该移除noexcept或处理所有可能的异常。
  5. 返回类型-> int 显式指定了返回类型为int
  6. lambda体:包含了具体的实现逻辑,包括修改捕获的变量、异常抛出和返回值计算。

请注意,由于我们在异常说明符中使用了noexcept,但实际上在lambda体内部有可能抛出异常,这在实际编程中是不推荐的。这个例子只是为了展示所有六个部分的用法,并不是一个好的编程实践。在实际应用中,应该确保异常说明符与lambda体的实际行为相匹配。

mutable详细解释 

在C++中,mutable关键字在lambda表达式中有特殊的意义。通常,当一个变量被lambda表达式以值的方式捕获时,这个变量在lambda内部是只读的,即你不能修改它的值。然而,如果你希望在lambda内部修改这个按值捕获的变量,你就需要在lambda表达式的定义中使用mutable关键字。

mutable关键字允许lambda表达式内部修改那些以值捕获方式捕获的外部变量。当你使用mutable时,lambda会创建这些变量的一个局部副本,你可以在lambda体内部自由地修改这些副本。

下面是一个简单的例子来说明mutable的用法:

#include <iostream>  
  
int main() {  
    int x = 10;  
  
    // 使用mutable关键字  
    auto lambda = [x]() mutable {  
        x = 20; // 修改按值捕获的x  
        std::cout << "Inside lambda: x = " << x << std::endl;  
    };  
  
    lambda(); // 调用lambda  
  
    std::cout << "Outside lambda: x = " << x << std::endl;  
  
    return 0;  
}

输出将是:

Inside lambda: x = 20  
Outside lambda: x = 10

在这个例子中,我们定义了一个lambda表达式,它按值捕获了变量x。由于我们使用了mutable关键字,我们可以在lambda体内部修改x的值。然而,这个修改只会影响lambda内部的x的副本,而不会影响外部的x变量。

需要注意的是,mutable关键字并不允许你修改以引用方式捕获的变量,因为这些变量本身就是对外部变量的引用,你可以直接修改它们而不需要mutable

此外,mutable还可以用于类的成员函数,表示该成员函数不会修改类的任何数据成员(即不会改变对象的状态),但这与lambda表达式中的mutable是完全不同的概念。在lambda表达式中,mutable仅与按值捕获的变量相关。

c++ lambda 表达式的可变说明符都有什么,分别表示什么意思 

在C++中,lambda表达式的可变说明符实际上只有一个,即mutable。它不是一个有多种形式的说明符,而是一个简单的关键字,用于改变lambda表达式内部对捕获的变量的处理方式。

mutable关键字的作用是允许lambda表达式内部修改那些以值捕获(通过拷贝)方式捕获的外部变量。通常,当lambda表达式按值捕获一个变量时,这个变量在lambda内部是只读的,因为它的值被复制到了lambda的一个局部副本中。然而,如果你希望在lambda内部修改这个副本,你就需要在lambda的定义中使用mutable关键字。

这里需要强调的是,mutable只影响按值捕获的变量。对于按引用捕获的变量,你可以在lambda内部直接修改它们,而不需要mutable关键字。

总结一下,mutable在C++的lambda表达式中表示:

  • 允许修改按值捕获的外部变量的副本。
  • 不影响按引用捕获的变量。

这是一个简单但强大的特性,因为它增加了lambda表达式的灵活性,使你能够在不改变外部变量本身的情况下,在lambda内部对变量的副本进行操作。然而,也应该谨慎使用mutable,因为它可能会导致代码更难理解和维护,特别是当lambda表达式变得复杂时。

c++ lambda 表达式的构成 

当然可以,以下是通过表格方式详细阐述的C++ lambda表达式的各个组成部分:

组成部分说明示例
捕获列表界定外部变量如何被lambda捕获:按值、按引用或混合方式。[x, &y](x按值,y按引用)
参数列表类似常规函数,定义lambda的输入参数。(int a, double b)
可变说明符可选,使用mutable允许修改按值捕获的变量。mutable
异常说明符可选,如noexceptthrow(),指明lambda可能抛出的异常类型。noexcept
返回类型可选,若省略则编译器自动推导;也可使用尾置返回类型进行明确指定。-> int
lambda体包含lambda的具体执行代码,类似于函数的函数体。{ return a + b; }

下面是一个结合了上述所有部分的C++ lambda表达式示例:

auto myLambda = [x, &y](int a, double b) mutable noexcept -> int {  
    x = a; // 修改按值捕获的x  
    y += b; // 修改按引用捕获的y  
    return x + static_cast<int>(y);  
};

在这个例子中:

  • 捕获列表:[x, &y],其中x按值捕获,y按引用捕获。
  • 参数列表:(int a, double b),定义了两个输入参数ab
  • 可变说明符:mutable,允许在lambda体内修改按值捕获的x
  • 异常说明符:noexcept,表明此lambda不会抛出异常。
  • 返回类型:-> int,明确指定了返回类型为int
  • lambda体:{ x = a; y += b; return x + static_cast<int>(y); },包含了具体的执行逻辑。

需要注意的是,在实际编程中,并不总是需要所有这些部分。例如,如果lambda不捕获任何外部变量,捕获列表可以为空;如果lambda不需要参数,参数列表也可以省略;如果lambda体足够简单,编译器通常能自动推导返回类型,从而无需显式指定。

c++ lambda 表达式的异常说明符都有什么,请举例分别说明 

在C++中,lambda表达式的异常说明符与常规函数的异常说明符非常相似。它们用于指定lambda可能抛出的异常类型,从而帮助调用者了解并处理这些潜在的异常。C++11及更高版本支持两种形式的异常说明符:

  1. noexcept(或noexcept(true):表示lambda不会抛出任何异常。
  2. 异常规格(exception specification):使用throw()throw(类型列表)来指定lambda可能抛出的异常类型。然而,需要注意的是,C++11之后,异常规格已被noexcept取代,并且使用异常规格可能会导致编译器发出警告或错误,因为它们是已废弃的特性。

下面是一些例子,分别展示了noexcept和旧的异常规格(尽管后者不推荐使用):

使用 noexcept

auto lambda = [](int x) noexcept {  
    // 此lambda保证不抛出异常  
    return x * 2;  
};

在这个例子中,noexcept说明符表明lambda不会抛出任何异常。如果lambda体内确实抛出了异常,那么程序将调用std::terminate()来终止执行。

使用旧的异常规格(不推荐)

auto lambda = [](int x) throw() {  
    // 此lambda应保证不抛出任何异常  
    return x * 2;  
};

或者指定可能抛出的异常类型:

#include <stdexcept>  
  
auto lambda = [](int x) throw(std::runtime_error) {  
    if (x < 0) {  
        throw std::runtime_error("Negative value not allowed");  
    }  
    return x * 2;  
};

在这个例子中,throw(std::runtime_error)说明符表明lambda只可能抛出std::runtime_error类型的异常。然而,这种旧的异常规格在C++11及更高版本中是不推荐的,应该使用noexcept或省略异常说明符(让编译器自动处理异常)。

总结

  • noexcept 是推荐使用的异常说明符,用于表明lambda不会抛出异常。
  • 旧的异常规格(如throw()throw(类型列表))在C++11及更高版本中已废弃,不建议使用。

在实际编程中,如果lambda有可能抛出异常,通常最好省略异常说明符,让编译器和调用者自行处理这些异常。如果确定lambda不会抛出异常,则可以使用noexcept来提供这一保证。

什么是异常规格,能详细介绍下吗 

异常规格(Exception Specification)是C++编程语言中用于声明函数可能抛出的异常类型的一种机制。它允许程序员在函数声明时明确指定该函数可能抛出的异常类型,从而增加代码的可读性和可维护性。异常规格通过特定的语法来实现,帮助调用者了解函数可能抛出的异常,并编写适当的异常处理代码。

异常规格的语法

在C++98中,异常规格使用throw(exception_type_list)语法来实现,其中exception_type_list是一个逗号分隔的异常类型列表。例如:

void func() throw(int, std::exception);

这表示func函数可能抛出int类型或std::exception类型及其派生类型的异常。

异常规格的意义

  1. 代码可读性:异常规格使得代码更容易理解,因为它明确地告诉调用者这个函数可能会抛出哪些类型的异常。

  2. 异常处理:有助于调用者编写适当的异常处理代码,从而避免在运行时遇到意外的异常。

  3. 函数维护:对于函数的维护者来说,异常规格也是一种约束,提示他们不要抛出未在规格中声明的异常类型。

异常规格的弃用与替代

需要注意的是,在C++11及以后的版本中,传统的异常规格语法(即使用throw(exception_type_list)的方式)已被弃用,并推荐使用noexcept关键字来声明函数是否抛出异常。例如:

void func() throw(int, std::exception);

这表示func函数不会抛出任何异常。如果函数确实抛出了异常,std::terminate()函数将被调用,程序将终止。

异常规格的使用注意事项

  1. 编译器检查:虽然编译器在编译时不会强制检查异常规格的一致性,但它在某些情况下能够检测到异常规格的不一致。

  2. 运行时行为:如果一个函数抛出一个不在异常规格范围内的异常,系统在运行时能够检测出这个错误,并自动调用std::unexpected()函数(在C++11之前)。在C++11及以后,由于noexcept的引入,这种机制有所变化。

  3. 兼容性:当新代码与没有异常规格的老代码整合在一起工作时,异常规格可以作为一种兼容性的手段。通过明确指定函数可能抛出的异常类型,新代码可以与老代码更好地协同工作。

总结

异常规格是C++中用于声明函数可能抛出的异常类型的一种机制,它有助于提高代码的可读性和可维护性。然而,随着C++语言的发展,传统的异常规格语法已被弃用,并推荐使用noexcept关键字来替代。在实际编程中,应根据所使用的C++标准来选择合适的异常声明方式。

Pass a context object as 2nd singleShot parameter [clazy-connect-3arg-lambda]

在 Qt 编程中,使用 QObject::connect 或相关的信号槽机制时,如果你使用了 lambda 表达式并且该 lambda 表达式中使用了外部变量,你需要特别注意 lambda 表达式的捕获方式。根据你提供的错误信息,CLazy 工具提示你在使用 QTimer::singleShot 时应该传递一个上下文对象作为第二个参数。

错误信息 [clazy-connect-3arg-lambda] 指出你在使用三参数的 connect 或 QTimer::singleShot 时,lambda 表达式可能不正确地捕获了变量,或者没有正确地指定上下文对象。这可能导致内存管理问题或程序崩溃。

解释和解决方案

当你使用 QTimer::singleShot 连接一个 lambda 表达式时,通常的用法是这样的:

QTimer::singleShot(ms, receiver, [=]() {  
    // 你的代码  
});

其中 ms 是延迟时间,receiver 是当 lambda 被执行时的上下文对象。如果你没有指定 receiver,lambda 中的任何对 this 的引用或对其他对象的引用可能会导致悬垂指针或未定义行为。

修改建议
  1. 确保传递正确的上下文对象:如果你的 lambda 表达式中使用了 this 或其他对象的成员,确保这些对象在 lambda 执行时仍然有效。通常,你可以将 this 作为上下文对象传递给 singleShot

修改后的代码可能看起来像这样:

QTimer::singleShot(ms, this, [=]() {  
    // 使用 this 或其他成员  
    this->someMethod();  
});
  1. 检查捕获方式:在 lambda 表达式中,使用 [=] 捕获所有外部变量,或使用 [&] 如果你只需要引用。确保捕获的方式符合你的需求,避免不必要的复制或悬挂引用。

  2. 确保对象生命周期:确保在 lambda 表达式执行时,传递给 singleShot 的上下文对象(如 this)仍然有效。如果对象可能在 lambda 执行前被销毁,你需要重新考虑你的设计。

通过上述修改,你的代码应该能够避免由 CLazy 报告的问题,并确保更稳定和安全的程序执行。

lambda表达式中的[=]和[&] 

在C++中,lambda表达式是一种定义匿名函数对象的方式,它可以在需要函数对象的地方使用,例如在算法、事件处理或异步编程中。lambda表达式可以捕获其定义范围内的变量,捕获方式通过捕获列表来指定。捕获列表是lambda表达式定义中的第一个部分,位于方括号[]内。

捕获列表中的[=][&]是两种常见的捕获方式:

  1. [=](值捕获):
    当使用[=]时,lambda表达式会捕获其定义范围内所有变量的副本(按值捕获)。这意味着在lambda表达式内部对捕获的变量所做的任何修改都不会影响原始变量。捕获的变量在lambda表达式被创建时复制,因此它们拥有独立的值。

    int x = 10;  
    auto lambda = [=]() {  
        // 这里x的值是10,对x的修改不会影响外部的x  
        x += 5;  
        std::cout << x << std::endl; // 输出15  
    };  
    lambda();  
    std::cout << x << std::endl; // 外部的x仍然是10
  2. [&](引用捕获):
    当使用[&]时,lambda表达式会捕获其定义范围内所有变量的引用(按引用捕获)。这意味着在lambda表达式内部对捕获的变量所做的修改将直接影响原始变量。引用捕获允许lambda表达式与外部变量保持同步。

     

    int x = 10;  
    auto lambda = [&]() {  
        // 这里x是原始变量的引用,对x的修改会影响外部的x  
        x += 5;  
        std::cout << x << std::endl; // 输出15  
    };  
    lambda();  
    std::cout << x << std::endl; // 外部的x现在是15

需要注意的是,当lambda表达式被复制或移动时,捕获的变量也会受到影响。对于值捕获,每个lambda表达式副本都有自己的捕获变量副本。对于引用捕获,所有lambda表达式实例都共享相同的外部变量引用。

此外,还可以混合使用值捕获和引用捕获,通过在捕获列表中明确指定变量名和其捕获方式来实现。例如,[=, &x]表示按值捕获除x以外的所有变量,并按引用捕获x

在选择捕获方式时,应考虑lambda表达式的生命周期、捕获变量的生命周期以及是否需要修改捕获的变量。如果lambda表达式的执行不会超出捕获变量的生命周期,并且需要修改原始变量,那么引用捕获可能是更好的选择。否则,值捕获可能更安全,因为它不会受到外部变量生命周期或修改的影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值