Lambda表达式(C++11 起)

来自cppreference.com
< cpp‎ | language

构造一个闭包:能够捕获作用域中变量的无名函数。

目录

[编辑] 语法

[ capture-list ] ( params ) mutable(可选) constexpr(可选)(C++17) exception attribute -> ret { body } (1)
[ capture-list ] ( params ) -> ret { body } (2)
[ capture-list ] ( params ) { body } (3)
[ capture-list ] { body } (4)

1) 完整声明。

2) const lambda的声明:无法修改按复制捕获的对象。

3) 省略尾随返回类型:闭包之operator()的返回类型根据下列规则确定:

(C++14 前)

return语句推导返回类型,如同返回类型声明为auto的函数。

(C++14 起)

4) 省略参数列表:函数不使用参数,如同参数列表为()。仅若constexpr、mutable、异常规定、属性,或尾随返回类型都不被使用,才能使用此形式。

[编辑] 解释

mutable - 允许body修改按复制捕获的参数,及调用其非const成员函数
constexpr(C++17) - 显示指定该函数调用运算符是constexpr函数。此运算符不存在时,若它恰好满足所有constexpr函数的要求,则它也还是constexpr
exception - 为闭包类型的operator()提供异常规定noexcept子句
attribute - 为闭包类型的operator()提供属性规定
capture-list - 零或更多个捕获的逗号分隔列表,可选地以capture-default开始。

捕获列表能按下列方式传递(详细描述见下方):

  • [a,&b] 其中a以复制捕获而b以引用捕获。
  • [this] 以引用捕获当前对象(*this
  • [&] 以引用捕获所有用于所有lambda体内的自动变量,并以引用捕获当前对象,若它存在
  • [=] 以复制捕获所有用于lambda体内的自动变量,并以引用捕获当前对象,若它存在
  • [] 无捕获
params - 参数列表,同具名函数一般,除了不允许默认参数 (C++14 前)若将auto用作参数的类型,则该lambda是泛型lambda (C++14 起)
ret - 返回类型。若不写,则为函数的return语句所隐喻(或若函数不返回任何值则为void)
body - 函数体

lambda表达式是一个纯右值表达式,其值为 (C++17 前)其结果对象为 (C++17 起)一个拥有通称“闭包类型”的,唯一、未命名、非联合体、非聚合体、类类型的无名临时对象,它被声明于(为了ADL目的)最小的容纳该lambda表达式的块作用域、类作用域、或命名空间作用域中。闭包类型拥有下列成员:

ClosureType::operator()(params)

ret operator()(params) const { body }
(不使用关键词mutable)
ret operator()(params) { body }
(使用关键词mutable)
template<template-params>
ret operator()(params) { body }
(C++14 起)
(泛型lambda)

在被调用时执行lambda表达式的函数体。访问一个变量时,对于以复制捕获的实体访问的是其副本,对于以引用捕获的实体访问的是其原始对象。除非在lambda表达式中使用mutable,否则函数调用运算符为const资格,而以复制捕获的对象不可自此operator()内部修改。函数调用运算符决不会是volatile资格或是虚的。

若函数调用运算符满足constexpr函数的要求,则它始终是constexpr。若在lambda生命中使用关键词constexpr,则它也是constexpr

(C++17 起)

对于每个params中类型指定为auto的参数,会以出现顺序添加一个虚设模板参数到template-params。虚设模板参数可以是参数包,若对应的params的函数参数成员是函数参数包。

// 泛型lambda,operator()是有二个参数的模板
auto glambda = [](auto a, auto&& b) { return a < b; };
bool b = glambda(3, 3.14); // ok
 
// 泛型lambda,operator()是有一个参数的模板
auto vglambda = [](auto printer) {
    return [=](auto&&... ts) // 泛型lambda,ts是参数包
    { 
        printer(std::forward<decltype(ts)>(ts)...);
        return [=] { printer(ts...); }; // 空参数lambda(不接收参数)
    };
};
auto p = vglambda([](auto v1, auto v2, auto v3) { std::cout << v1 << v2 << v3; });
auto q = p(1, 'a', 3.14); // 打印1a3.14
q();                      // 打印1a3.14

ClosureTypeoperator()不能被显式实例化或显式特化。

(C++14 起)

lambda表达式上的异常规定exception应用到函数调用运算符或运算符模板。

为了名称查找之需要、确定this指针的值和类型,以及为了访问非static类成员,闭包类型的函数调用运算符的函数体被设定在lambda表达式的语境之中。

struct X {
    int x, y;
    int operator()(int);
    void f()
    {
        // 下列lambda的语境是成员函数X::f
        [=]()->int
        {
            return operator()(this->x + y); // X::operator()(this->x + (*this).y)
                                            // this拥有类型X*
        };
    }
};

ClosureTypeoperator()不能在友元声明中指名。

悬垂引用

若隐式或显式地以引用捕获一个非引用实体,且在该实体的生存期结束后调用闭包对象的函数调用运算符,则发生未定义行为。C++闭包不会延长被捕获引用的生存期。

同样的规则应用于被捕获的this指针所指向对象的生存期。

ClosureType::operator ret(*)(params)()

(无捕获非泛型lambda)
using F = ret(*)(params);
operator F() const;
(C++17 前)
using F = ret(*)(params);
constexpr operator F() const;
(C++17 起)
(无捕获泛型lambda)
template<template-params> using fptr_t = /*see below*/;
template<template-params> operator fptr_t<template-params>() const;
(C++14 起)
(C++17 前)
template<template-params> using fptr_t = /*see below*/;
template<template-params> operator fptr_t<template-params>() const;
(C++17 起)

用户定义转换函数仅若lambda表达式的捕获列表为空才得到定义。它是闭包类型的公开、constexpr (C++17 起)、非虚、非explicit、const、noexcept (C++14 起)的成员函数。

泛型无捕获lambda拥有用户定义转换函数模板,它拥有与函数调用运算符相同的虚设模板参数列表。若返回类型为空或auto,则它为函数模板特化上的返回类型推导所包含,换言之,即是为转换函数模板的模板参数推导所包含。

void f1(int (*)(int)) {}
void f2(char (*)(int)) {}
void h(int (*)(int)) {} // #1
void h(char (*)(int)) {} // #2
auto glambda = [](auto a) { return a; };
f1(glambda); // ok
f2(glambda); // 错误:非可转换
h(glambda); // ok:调用#1因为#2不可转换
 
int& (*fpi)(int*) = [](auto* a)->auto& { return *a; }; // ok
(C++14 起)

此转换函数的返回值是指向一个拥有C++语言链接的函数的指针,即在调用该函数时,拥有与直接调用该闭包对象的函数调用运算符相同的效应。

若函数调用运算符(或对于泛型lambda的特化)为constexpr,则此函数为constexpr。

auto Fwd= [](int(*fp)(int), auto a){return fp(a);};
auto C=[](auto a){return a;};
static_assert(Fwd(C,3)==3);//OK
auto NC=[](auto a){static int s; return a;};
static_assert(Fwd(NC,3)==3); // error:因为s故没有特化能成为constexpr

若闭包对象的operator()拥有不抛出异常规定,则此函数所返回的指针拥有指向noexcept函数的指针类型。

(C++17 起)

ClosureType::ClosureType()

ClosureType() = delete;
(C++14 前)
ClosureType(const ClosureType& ) = default;
(C++14 起)
ClosureType(ClosureType&& ) = default;
(C++14 起)

闭包类型非可默认构造DefaultConstructible)。闭包类型有被删除的 (C++14 前) (C++14 起)默认构造函数。复制构造函数和移动构造函数被隐式声明 (C++14 前)声明为默认 (C++14 起),而且可以根据复制构造函数移动构造函数的通常规则隐式定义。

ClosureType::operator=(const ClosureType&)

ClosureType& operator=(const ClosureType&) = delete;

闭包类型非可复制赋值CopyAssignable)。

ClosureType::~ClosureType()

~ClosureType() = default;

析构函数是隐式声明的。

ClosureType::Captures

T1 a;

T2 b;

...

若lambda表达式以复制捕获任何对象(隐式地以捕获子句[=]或显式地用不包含字符&的捕获,例如[a, b, c]),则闭包类型包含无名数据成员,以未指定顺序声明,它们保有被如此捕获的所有实体的副本。

对应无初始化器捕获的数据成员,在求值lambda表达式时被直接初始化。对应带初始化的捕获者,按照初始化器要求初始化(可以是复制或直接初始化)。若捕获数组,则数组元素按照递增顺序初始化。数据成员初始化的顺序是它们被声明的顺序(该顺序未指定)。

每个数据成员的类型是对应被捕获实体的类型,除非该实体拥有引用类型(在该情况下,到函数的引用被捕获作到被引用函数的左值引用,而到对象的引用被捕获作被引用对象的副本)。

对于以引用捕获的实体(以默认捕获[&]或使用字符&时,例如[&a, &b, &c]),是否在闭包类型中声明附加数据成员是未指定的,但任何这种附加成员必须满足字面类型LiteralType)要求 (C++17 起)

Lambda表达式不允许在不求值表达式模板实参别名声明typedef声明,及任何函数中的函数体和函数默认参数以外处出现。

[编辑] Lambda捕获

capture-list是零或多个捕获的逗号分隔列表,可选地以capture-default开始。仅有的捕获默认是

  • & (以引用隐式捕获odr使用的自动变量和*this),与
  • = (以复制捕获odr使用的自动变量和以引用隐式捕获*this,若odr使用this)。

capture-list单个捕获的语法是

identifier (1)
identifier ... (2)
identifier initializer (3) (C++14)
& identifier (4)
& identifier ... (5)
& identifier initializer (6) (C++14)
this (7)
* this (8) (C++17)
1) 简单的以复制捕获
2) 作为参数展开的以复制捕获
3)初始化器的以复制捕获
4) 简单的以引用捕获
5) 作为参数展开的以引用捕获
6) 带初始化器的以引用捕获
7) 当前对象的以引用捕获
8) 当前对象的以复制捕获

若默认捕获选项是&,则后继捕获必须不以&开始。若默认捕获是=,则后继捕获必须以&开始或是*this (C++17 起)。任何捕获只可以出现一次。

struct S2 { void f(int i); };
void S2::f(int i)
{
    [&]{}; //ok:默认捕获以引用
    [=]{}; //ok:默认捕获以复制
    [&, i]{}; // ok:以引用捕获,除了i以复制捕捉
    [=, &i]{}; // ok:以复制捕获,除了i以引用捕获
    [&, &i] {}; // 错误:以引用为默认时再以引用捕获
    [=, this] {}; // 错误:=为默认时的this
    [=, *this]{}; // ok:以复制捕获外围的S2 (C++17)
    [i, i] {}; // 错误:i重复
    [this, *this] {}; // 错误:“this”重复 (C++17)
}

只有定义于块作用域中的lambda表达式能拥有默认捕获选项或不带初始化器的捕获。对于这种lambda表达式,定义触及作用域为包围它的作用域的集合,直到并含有包围它的最内层函数(及其参数)。这包含嵌套的块作用域及包围它的lambda表达式的作用域,若此lambda被嵌套。

任何无初始化器的捕获(除了this捕获)的identifier触及作用域中使用通常的无限定名称查找来查找。查找的结果必须是一个触及作用域中拥有自动存储期的变量。该变量(或this)被显式捕获

带初始化器的捕获表现如同它声明并显式捕获一个以auto类型声明的变量,其声明性区域是lambda函数体(即它在其初始化器中不处于作用域内),除非:

  • 若捕获为以复制,则该闭包对象的非静态数据成员是另一种表示该自动变量的方式。
  • 若捕获为以引用,则引用变量的生存期在闭包对象的生存期结束时结束。

这被用于用捕获捕捉仅移动类型,例如x = std::move(x)

int x = 4;
auto y = [&r = x, x = x + 1]()->int
    {
        r += 2;
        return x * x;
    }(); // 更新::x为6并初始化y为25。
(C++14 起)

若捕获列表拥有默认捕获选项,且不显式捕获包围它的对象(如this*this)或一个自动变量,则捕获列表隐式捕获它,若

  • lambda的函数体odr使用该变量或this指针
  • 或者,在依赖泛型lambda参数的表达式中,该变量或this指针在潜在求值的表达式中被指名
void f(int, const int (&)[2] = {}) {} // #1
void f(const int&, const int (&)[1]) {} // #2
void test()
{
    const int x = 17;
    auto g1 = [](auto a) { f(x); }; // ok:调用#1,不捕获x
    auto g2 = [=](auto a) {
            int selector[sizeof(a) == 1 ? 1 : 2] = {};
            f(x, selector); // ok:是依赖表达式,故捕获x
    };
}
(C++14 起)

若lambda的函数体odr使用一个以复制捕获的实体,则访问该闭包类型的成员。若它不odr使用该实体,则访问是到原对象的:

void f(const int*);
void g()
{
    const int N = 10;
    [=]{ 
        int arr[N]; // 非odr使用:表示g的const int N
        f(&N); // odr使用:导致N被捕获(以复制)
               // &N是闭包对象的成员N的地址,不是g的N
    }();
}
若lambda表达式odr使用以引用捕获的引用,则它在使用被引用到的原对象,而非被捕获的引用本身:
#include <iostream>
 
auto make_function(int& x) {
  return [&]{ std::cout << x << '\n'; };
}
 
int main() {
  int i = 3;
  auto f = make_function(i); // x 的使用直接绑定到 i
  i = 5;
  f(); // OK ;打印 5
}


在lambda体内,在任何拥有自动存储期的变量上的任何decltype使用表现类似它被捕获并被odr使用,即使decltype自身不是odr使用,而且没有发生实际捕获:

void f3() {
    float x, &r = x;
    [=]
    { // x和r未被捕获(decltype运算数中的出现不是odr使用)
        decltype(x) y1; // y1拥有类型float
        decltype((x)) y2 = y1; // y2拥有类型float const&因为此lambda不可变而且x是左值
        decltype(r) r1 = y1;   // r1拥有类型float&(不考虑变换)
        decltype((r)) r2 = y2; // r2拥有类型float const&
    };
}

任何为lambda捕获的实体(隐式或显式)为该lambda表达式所odr使用(从而,嵌套lambda的隐式捕获触发外层lambda中的隐式捕获)。

任何隐式捕获变量必须声明于该lambda的触及作用域中。

若lambd捕获外围它的对象(如this*this),则最近的外围函数必须是非静态成员函数:

struct s2 {
  double ohseven = .007;
  auto f() { // 下面二个lambda的最近的外围函数
    return [this] { // 以引用捕获外围的s2
      return [*this] { // 以复制捕获外围的s2 (C++17)
          return ohseven;// OK
       }
     }();
  }
  auto g() {
     return []{ // 无捕获
         return [*this]{};// 错误:*this未为外层lambda表达式所捕获
      }();
   }
};

若lambda表达式(或泛型lambda表达式的函数调用运算符的实例化)ODR使用this或任何拥有自动存储期的变量,则它必须被lambda表达式捕获。

void f1(int i)
{
    int const N = 20;
    auto m1 = [=] {
            int const M = 30;
            auto m2 = [i] {
                    int x[N][M]; // N和M非odr使用
                                 // (OK从而它们不被捕获)
                    x[0][0] = i; // i为m2显示捕获
                                 // 并为m1隐式捕获m1
            };
    };
 
    struct s1 // f1()中的局部类
    {
        int f;
        void work(int n) // 非静态成员函数
        {
            int m = n * n;
            int j = 40;
            auto m3 = [this, m] {
                auto m4 = [&, j] { // 错误:j未被m3捕获
                        int x = n; // 错误:n被m4隐式捕获但未被m3捕获
                        x += m;    // ok:m被m4隐式捕获并被m3显式捕获
                        x += i;    // 错误:i在触及作用域之外(触及作用域终于work())
                        x += f;    // ok:this被m4隐式捕获并被m3显式捕获
                };
            };
        }
    };
}

类成员不能被显式捕获而不带初始化器(如上所提示,捕获列表中仅容许变量):

class S {
  int x = 0;
  void f() {
    int i = 0;
//  auto l1 = [i, x]{ use(i, x); };    // 错误:x不是变量
    auto l2 = [i, x=x]{ use(i, x); };  // OK,复制捕获
    i = 1; x = 1; l2(); // calls use(0,0)
    auto l3 = [i, &x=x]{ use(i, x); }; // OK,引用捕获
    i = 2; x = 2; l3(); // calls use(1,2)
  }
};

当lambda使用隐式以值捕获捕获变量时,它不制造成员变量的副本:成员变量m的使用被当做表达式(*this).m,取而代之被(以值)捕获的是this指针。成员表现如同以引用捕获:

class S {
  int x = 0;
  void f() {
    int i = 0;
    auto l1 = [=]{ use(i, x); }; // 捕获i的副本和this指针的副本
    i = 1; x = 1; l1(); // 调用use(0,1),如同i以复制而x以引用
    auto l2 = [i, this]{ use(i, x); }; // 同上,令其为显式
    i = 2; x = 2; l2(); // 调用use(1,2),如同i以复制而x以引用
    auto l3 = [&]{ use(i, x); }; // 以引用捕获i并捕获this指针的副本
    i = 3; x = 2; l3(); // 调用use(3,2),如同i和x都以引用
    auto l4 = [i, *this]{ use(i, x); }; // 制造*this的副本,包含x的副本
    i = 4; x = 4; l4(); // 调用use(3,2),如同i和x都以复制
  }
};

若lambda表达式出现于默认参数中,则它不能显式或隐式捕获任何变量。

无法捕获匿名联合体的成员。

若嵌套的m2捕获被立即包围它的lambda m1所捕获的某变量,则m2的捕获按照下列方式变形:

  • 若外围lambda m1以复制捕获,则m2捕获m1的闭包类型的非静态成员,而非原变量或this
  • 若外围lambda m1以引用捕获,则m2捕获原变量或this
#include <iostream>
 
int main()
{
    int a = 1, b = 1, c = 1;
 
    auto m1 = [a, &b, &c]() mutable {
        auto m2 = [a, b, &c]() mutable {
            std::cout << a << b << c << '\n';
            a = 4; b = 4; c = 4;
        };
        a = 3; b = 3; c = 3;
        m2();
    };
 
    a = 2; b = 2; c = 2;
 
    m1();                             // 调用m2()并打印123
    std::cout << a << b << c << '\n'; // 打印234
}


[编辑] 示例

此例演示(a)如何传递lambda给泛型算法;(b)生成自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';
 
    // 闭包的类型无法被指名,但可以用auto推断
    auto func1 = [](int i) { return i + 4; };
    std::cout << "func1: " << func1(6) << '\n';
 
    // 同所有可调用对象,闭包可捕获于std::function
    // (这可能出现不必要的开销)
    std::function<int(int)> func2 = [](int i) { return i + 4; };
    std::cout << "func2: " << func2(6) << '\n';
}

输出:

c: 5 6 7
func1: 10
func2: 10

[编辑] 缺陷报告

以下行为变化的缺陷报告可追溯至以前发布的 C++ 标准。

报告 适用于 已发布的行为 正确行为
CWG 1891 C++14 闭包拥有被delete的默认构造函数和隐式的复制/移动构造函数 没有默认构造函数,有default的复制/移动构造函数
CWG 1722 C++14 无捕获lambda的转换函数拥有未指定的异常规定 转换函数为noexcept

[编辑] 参阅

auto指定符 指定由表达式定义的类型(C++11) [编辑]
(C++11)
包装带有指定的函数调用签名的任何类型的可调用对象
原文:
wraps callable object of any type with specified function call signature
这段文字是通过 Google Translate 自动翻译生成的。
您可以帮助我们检查、纠正翻译中的错误。详情请点击这里

(类模板) [编辑]