Lambda 表达式

构造闭包:能够捕获作用域中的变量的无名函数对象。since C++11


语法

[ captures ] ( params ) specs requires(optional) { body } (1)
[ captures ] { body } (2) (until C++23)
[ captures ] specs { body } (2) (since C++23)
[ captures ] < tparams > requires(optional) ( params ) specs requires(optional) { body } (3) (since C++20)
[ captures ] < tparams > requires(optional) { body } (4) (since C++20)
(until C++23)
[ captures ] < tparams > requires(optional) specs { body } (4) (since C++23)
  1. 完整声明。
  2. 省略形参列表:函数不接收实参,如同形参列表是 ()
  3. 与 1 相同,但指定泛型 lambda 并显式提供模板形参列表。
  4. 与 2 相同,但指定泛型 lambda 并显式提供模板形参列表。

解释

  • captures - 包含零或更多个捕获符的逗号分隔列表,可以 默认捕获符(capture-default) 起始。 有关捕获符的详细描述,见下文。 如果变量满足下列条件,那么 lambda 表达式在使用它前不需要先捕获:
    • 该变量是非局部变量,或具有静态或线程局部存储期(此时无法捕获该变量),或者
    • 该变量是以常量表达式初始化的引用。
      如果变量满足下列条件,那么 lambda 表达式在读取它的值前不需要先捕获:
    • 该变量具有 const 而非 volatile 的整型或枚举类型,并已经用常量表达式初始化,或者
    • 该变量是 constexpr 的且没有 mutable 成员。
  • tparams - (角括号中的)模板形参列表,用于为泛型 lambda 提供各模板形参的名字(见下文的 ClosureType::operator())。与在模板声明中相似,模板形参列表可以后附 requires 子句,它指定各模板实参上的约束。 模板形参列表不能为空(不允许 <>)。
  • params - 形参列表,如在具名函数中。
  • specs - 由 specifiersexceptionattrtrailing-return-type 按顺序组成,每个组分均非必需
  • specifiers - 可选的说明符的序列。不提供说明符时复制捕获的对象在 lambda 函数体内是 const 的。可以使用下列说明符:
    • mutable: 允许 函数体 修改复制捕获的对象,以及调用它们的非 const 成员函数
    • constexpr: 显式指定函数调用运算符或运算符模板的任意特化为 constexpr 函数。如果没有此说明符但函数调用运算符或任意给定的运算符模板特化恰好满足针对 constexpr 函数的所有要求,那么它也会是 constexpr 的。 (C++17 起)
    • consteval:指定函数调用运算符或任意给定的运算符模板特化为立即函数。不能同时使用 consteval 和 constexpr。 (C++20 起)
  • exception - 为闭包类型的 operator() 提供动态异常说明或 (C++20 前) noexcept 说明符
  • attr - 为闭包类型的函数调用运算符或运算符模板的类型提供属性说明。这样指定的任何属性均属于函数调用运算符或运算符模板的类型,而非其自身。(例如不能使用 [[noreturn]])
  • trailing-return-type - -> 返回类型,其中 返回类型 指定返回类型。如果没有 尾随返回类型,那么闭包的 operator() 的返回类型从 return 语句推导,如同对于声明返回类型为 auto 的函数的推导一样。
  • requires - (C++20 起)向闭包类型的 operator() 添加约束
  • body - 函数体

当以 auto 为形参类型或显式提供模板形参列表 (C++20 起)时,该 lambda 是泛型 lambda。(C++14 起)

Lambda 捕获

捕获 是一个含有零或更多个捕获符的逗号分隔列表,可以 默认捕获符 开始。默认捕获符只有

  • &(以引用隐式捕获被使用的自动变量)和
  • =(以复制隐式捕获被使用的自动变量)。

当出现任一默认捕获符时,都能隐式捕获当前对象(*this)。若隐式捕获它,则始终以引用捕获,即使默认捕获符是 =。当默认捕获符为 = 时,*this 的隐式捕获被弃用。 (C++20 起)

捕获 中单独的捕获符的语法是:

标识符 (1)
标识符 ... (2)
标识符 初始化器 (3) (C++14 起)
& 标识符 (4)
& 标识符 ... (5)
& 标识符 初始化器 (6) (C++14 起)
this (7)
* this (8) (C++17 起)
... 标识符 初始化器 (9) (C++20 起)
& ... 标识符 初始化器 (10) (C++20 起)
  1. 简单的以复制捕获
  2. 作为包展开的简单的以复制捕获
  3. 带初始化器的以复制捕获
  4. 简单的以引用捕获
  5. 作为包展开的简单的以引用捕获
  6. 带初始化器的以引用捕获
  7. 当前对象的简单的以引用捕获
  8. 当前对象的简单的以复制捕获
  9. 初始化器为包展开的以复制捕获
  10. 初始化器为包展开的以引用捕获

当默认捕获符是 & 时,后继的简单捕获符不能以 & 开始。

struct S2 { void f(int i); };
void S2::f(int i)
{
    [&]{};          // OK:默认以引用捕获
    [&, i]{};       // OK:以引用捕获,但 i 以值捕获
    [&, &i] {};     // 错误:以引用捕获为默认时的以引用捕获
    [&, this] {};   // OK:等价于 [&]
    [&, this, i]{}; // OK:等价于 [&, i]
}

当默认捕获符是 = 时,后继的简单捕获符必须以 & 开始,或者为 *this (C++17 起) 或 this (C++20 起)。

struct S2 { void f(int i); };
void S2::f(int i)
{
    [=]{};          // OK:默认以复制捕获
    [=, &i]{};      // OK:以复制捕获,但 i 以引用捕获
    [=, *this]{};   // C++17 前:错误:无效语法
                    // C++17 起:OK:以复制捕获外围的 S2
    [=, this] {};   // C++20 前:错误:= 为默认时的 this
                    // C++20 起:OK:同 [=]
}

任何捕获符只可以出现一次:

struct S2 { void f(int i); };
void S2::f(int i)
{
    [i, i] {};        // 错误:i 重复
    [this, *this] {}; // 错误:"this" 重复 (C++17)
}

仿函数

在C++11之前,STL中的一些算法需要使用一种函数对象---仿函数(functor);其本质是重新定义和成员函数 operator() ,使其使用上很像普通函数,其实,细心的我们已经发现,Lambda函数与仿函数似乎有一些默契。 如下例子:折扣

class Price
{
private:
    float _rate;
public:
    Price(float rate):_rate(rate){}
    float operator()(float price)
    {
        return price*(1 - _rate / 100);
    }
};

int main(int argc, char* argv[])
{
    float rate=5.5f;

    Price c1(rate);
    auto c2 = [rate](float price)->float {return price*(1 - rate / 100); };

    float p1 = c1(3699);    //仿函数
    float p2 = c2(3699);    //Lambda函数

    return 0;
}

仿函数以rate初始化,Lambda捕捉rate变量,参数传递上,两者一致。

事实上,仿函数就是实现Lambda函数一种方式,编译器通常会把Lambda函数转换为一个仿函数对象,但是仿函数的语法却给我们带来了很大的便捷。

在C++11中,Lambda函数被广泛使用,很多仿函数被取代。

示例

int main(int argc, char* argv[])
{
    int a = 5, b = 7;
    auto total = [](int x, int y)->int {return x + y; };    //接受两个参数
    cout << total(a, b)<<endl;  //12
    auto fun1 = [=] {return a + b; };   //值传递捕捉父作用域变量
    cout << fun1() << endl; //12
    auto fun2 = [&](int c) {b = a + c; a = 1; };    //省略了返回值类型,引用捕获所有
    fun2(3);
    cout << a <<" "<< b << endl; //1 8
    a = 5; b = 7;   //被修改后,重新赋值
    auto fun3 = [=, &b](int c) mutable {b = a + c; a = 1; };    //以值传递捕捉的变量,在函数体里如果要修改,要加mutaple,因为默认const修饰
    fun3(3);
    cout << a << " " <<b<< endl;    //5 8
    a = 5; b = 7;   //被修改后,重新赋值
    auto fun4 = [=](int x, int y) mutable->int {a += x; b += y; return a + b; };
    int t = fun4(10, 20);
    cout << t << endl;  //42
    cout << a <<" "<< b << endl;    //5 7
    return 0;
}

块作用域以外的Lambda函数捕捉列表必须为空,因此这样的函数除了语法上的不同,和普通函数区别不大。

块作用域以内的Lambda函数仅能捕捉块作用域以内的自动变量,捕捉任何非此作用域或非自动变量(静态变量),都会引起编译器报错。

下面示例演示如何传递 lambda 给泛型算法,以及 lambda 表达式所产生的对象能如何存储于 std::function 对象。

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

int main()
{
    std::vector<int> c = {1, 2, 3, 4, 5, 6, 7};
    int x = 5;
    c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; }), c.end());

    std::cout << "c: ";
    std::for_each(c.begin(), c.end(), [](int i){ std::cout << i << ' '; });
    std::cout << '\n';

    // the type of a closure cannot be named, but can be inferred with auto
    // since C++14, lambda could own default arguments
    auto func1 = [](int i = 6) { return i + 4; };
    std::cout << "func1: " << func1() << '\n';

    // like all callable objects, closures can be captured in std::function
    // (this may incur unnecessary overhead)
    std::function<int(int)> func2 = [](int i) { return i + 4; };
    std::cout << "func2: " << func2(6) << '\n';

    constexpr int fib_max {8};
    std::cout << "Emulate `recursive lambda` calls:\nFibonacci numbers: ";
    auto nth_fibonacci = [](int n)
    {
        std::function<int(int, int, int)> fib = [&](int n, int a, int b)
        {
            return n ? fib(n - 1, a + b, a) : b;
        };
        return fib(n, 0, 1);
    };

    for (int i{1}; i <= fib_max; ++i)
    {
        std::cout << nth_fibonacci(i) << (i < fib_max ? ", " : "\n");
    }

    std::cout << "Alternative approach to lambda recursion:\nFibonacci numbers: ";
    auto nth_fibonacci2 = [](auto self, int n, int a = 0, int b = 1) -> int
    {
        return n ? self(self, n - 1, a + b, a) : b;
    };

    for (int i{1}; i <= fib_max; ++i)
    {
        std::cout << nth_fibonacci2(nth_fibonacci2, i) << (i < fib_max ? ", " : "\n");
    }

#ifdef __cpp_explicit_this_parameter
    std::cout << "C++23 approach to lambda recursion:\n";
    auto nth_fibonacci3 = [](this auto self, int n, int a = 0, int b = 1)
    {
         return n ? self(n - 1, a + b, a) : b;
    };

    for (int i{1}; i <= fib_max; ++i)
    {
        std::cout << nth_fibonacci3(i) << (i < fib_max ? ", " : "\n");
    }
#endif
}

Possible output:

c: 5 6 7 
func1: 10
func2: 10
Emulate `recursive lambda` calls:
Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13
Alternative approach to lambda recursion:
Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13

参考资料

Lambda expressions: https://en.cppreference.com/w/cpp/language/lambda

Lambda expressions: https://zh.cppreference.com/w/cpp/language/lambda