六一的部落格


关关难过关关过,前路漫漫亦灿灿。



需要知道定义lambda背后, 编译器所做的工作


局部变量的捕获方式

-
值捕获 函数体内使用的是变量的同名拷贝
引用捕获 函数体内使用的是变量的同名引用

值捕获

由于被捕获变量的值是在lambda创建时拷贝,之后对其执行写操作不会影响到lambda内的同名拷贝

函数体内为v1的同名拷贝:

  • 无法对其执行写操作
  • 修改局部变量v1的值, 不会影响同名拷贝的值
 1void fcn1()
 2{
 3    size_t v1 = 42;
 4
 5    auto f = [v1]{ return v1; };       
 6    // 此时是值捕获,v1为局部变量v1的同名拷贝, 其值为42, 此例中不会发生改变
 7
 8    v1 = 0;
 9    auto j = f();
10    // j = 42
11}

引用捕获

当我们在lambda函数体内使用变量时,实际使用的是引用所绑定的对象

函数体内为v1的同名引用

 1void fcn2()
 2{
 3    size_t v1 = 42;
 4    auto f2 = [&v1] { return v1; }
 5    // 此时是引用捕获,数据成员保存v1的引用而非拷贝
 6
 7    v1 = 0;
 8    auto j = f2();
 9    // j = 0
10}

示例

biggies函数接受一个ostream的引用,用来输出数据;并接受一个字符作为分隔符

 1void biggies(vector<string> &words,
 2                vector<string>::size_type sz,
 3                ostream &os = cout, char c = ' ')
 4{
 5    // ...
 6
 7    for_each(words.begin(), words.end(),
 8            [&os, c](const string &s) { os << s << c; });
 9}
10
11// ostream对象不支持拷贝: 传递ostream对象的方法有两个, 通过引用和指针

如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda调用的时候是存在的


引用捕获的返回

函数可以返回lambda

如果该lambda使用了引用捕获, 其引用捕获的所有局部变量均无效


避免捕获指针和引用捕获


隐式捕获

除了显式给出lambda需要捕获的变量, lambda还支持批量捕获

-
= 对未显式给出名称的变量进行值捕获
& 对未显式给出名称的变量进行引用捕获

可以混用隐式捕获和显式捕获,对一部分变量采用值捕获,对另一部分变量采用引用捕获

混用隐式捕获和显式捕获时

  1. 捕获列表中的第一个元素必须是&或=, 即默认捕获方式
  2. 显式和隐式必须采用不同的捕获方式: 如果隐式捕获使用引用方式,那么显式捕获必须使用值捕获,不得在名字前加 &

捕获列表的6种形式

  1. 捕获列表为空

    []
  2. 显式捕获

    值捕获给出变量名, 引用捕获在变量名之前加上 &

    [identifier_list]
  3. 隐式值捕获

    值捕获所有可访问的局部变量

    [=]
  4. 隐式引用捕获

    引用捕获所有可访问的局部变量

    [&]
  5. 显式值捕获 + 隐式引用捕获: 给出需值捕获的变量名称, 引用捕获余下可访问的局部变量

    identifier_list里的变量均采用值捕获

    [&, identifier_list]
  6. 显式引用捕获 + 隐式值捕获: 给出需引用捕获的变量名称, 值捕获余下可访问的局部变量

    identifier_list里的变量均采用引用捕获,名字前一定有&

    [=, identifier_list]

隐式值捕获

1wc = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() >= sz; });

混用隐式捕获和显式捕获

1void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ')
2{
3    // os为隐式引用捕获;c为显式值捕获
4    for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c; });
5
6    // os为显式引用捕获;c为隐式值捕获
7    for_each(words.begin(), words.end(), [=, &os] { os << s << c; });
8}

定义lambda时, 编译器在背后所做的工作


定义lambda时

部分内容为猜测

  1. 值捕获的局部变量

    在类中定义了数据成员, 为局部变量的拷贝

  2. 引用捕获的局部变量

    书上说无需将其存储为数据成员 P508

    在类中定义了引用类型数据成员, 绑定局部变量对应的对象

  3. 为该类型重载了调用运算符

    调用运算符的重载版本是const成员函数

    lambda的参数为调用运算符的参数, lambda的函数体为调用运算符的实现

    函数体内使用的为自己的数据成员. 因为是const成员函数, 无法对值捕获的变量执行写操作

编译器根据lambda的定义生成了一个类型, 并创建了该类型的一个对象

创建该类对象时, 数据成员被初始化

建议减少捕获的数据量, 都是要占用内存的


向函数传递lambda时

同时定义了一个新类型与该类型的一个对象

传递的是编译器生成的类类型的未命名对象


使用auto定义一个用lambda初始化的变量时

定义了一个根据lambda生成的类型的对象

对对象使用调用运算符时, 才会执行lambda表达式的函数体


可变lambda

mutable

类的成员函数那里, 如果将数据成员声明为 mutable , 在const成员函数中也可对这些数据成员执行写操作

猜测: 书上已证实

  • lambda对应类型的调用运算符重载为const成员函数

猜测: 书上说调用运算符不再为const P508

如果将lambda声明为 muable , 编译器定义类型时, 会使用值捕获的局部变量初始化可变数据成员


声明lambda表达式时未使用mutable, 不可对值捕获的同名拷贝执行写操作

1size_t v1 = 42;
2auto f = [v1]() { return ++v1; };    // 报错
3v1 = 0;
4auto j = f();
error: cannot assign to a variable captured by copy in a non-mutable lambda

可以对引用捕获的同名引用执行写操作

引用不具有底层const

 1size_t v1 = 42;
 2
 3auto f = [&v1]() { return ++v1; };
 4
 5// v1 = 42
 6
 7auto j = f();
 8
 9// v1 = 43, j = 43
10
11v1 = 0; // 修改引用捕获的变量的值
12
13auto j2 = f();        // j2 = 1, v1 = 1

可以在const成员函数中对引用类型数据成员执行写操作

 1#include <iostream>
 2
 3using namespace std;
 4
 5struct test
 6{
 7    int &a;
 8    test(int &aa) : a(aa) {}
 9
10    void printA() { cout << a << endl; }
11    void increaseA() const { ++a; }
12};
13
14int main()
15{
16    int a = 5;
17    test t(a);
18
19    t.printA();
20    t.increaseA();
21    t.printA();
22    cout << a << endl;
23    return 0;
24}
25
26// 5 6 6

声明lambda表达式时使用mutable关键字, 可以对值捕获的同名拷贝执行写操作

mutable关键字在形参列表之后给出

 1void fcn3()
 2{
 3    size_t v1 = 42;
 4
 5    auto f = [v1]() mutable { return ++v1; };
 6
 7    // v1 = 42
 8
 9    auto j = f();
10
11    // j = 43,lambda对象中的v1为43
12
13    auto k = f();
14
15    // k = 44,lambda对象中的v1为44
16}

可变lambda省略参数列表

仅作了解

  1. lambda表达式支持省略参数列表

    没有问题
    1auto g = [] { return 42; };
  2. 使用mutable关键字时
    • 没有问题

      1auto f = [v1] () mutable { return ++v1; };
    • 有告警

      1auto f = [v1] mutable { return ++v1; };
      warning: lambda without a parameter clause is a C++2b extension

lambda使用尾置返回

  1. 省略返回类型时

    • 如果lambda表达式的函数体只包含单一return语句, 编译器根据返回值中推断出返回类型

      1// 工作正常,对每个元素取绝对值
      2
      3transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i < 0 ? -i : i; });
    • 如果lambda表达式包含return语句外的任何语句, 编译器认为其返回类型为void

      1// 编译错误:除return语句外还有其他语句,编译器认为该lambda返回类型为void,此时不能有返回值
      2
      3transform(vi,begin(), vi.end(), vi.begin(), [](int i) { if (i < 0) return -i; else return i; });
  2. 使用尾置返回

    1transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; });

创建新序列, 为给定序列的映射

transform

给出序列1的所有元素, 使用迭代器范围: [b, e)

给出指向序列2首元素的迭代器

给出一元谓词: 对序列1中的每个元素调用给定的一元谓词, 将返回值写入序列2

指向序列2首元素的迭代器可以是b


lambda捕获和返回


需要知道定义lambda背后, 编译器所做的工作


局部变量的捕获方式

-
值捕获 函数体内使用的是变量的同名拷贝
引用捕获 函数体内使用的是变量的同名引用

值捕获

由于被捕获变量的值是在lambda创建时拷贝,之后对其执行写操作不会影响到lambda内的同名拷贝

函数体内为v1的同名拷贝:

  • 无法对其执行写操作
  • 修改局部变量v1的值, 不会影响同名拷贝的值
 1void fcn1()
 2{
 3    size_t v1 = 42;
 4
 5    auto f = [v1]{ return v1; };       
 6    // 此时是值捕获,v1为局部变量v1的同名拷贝, 其值为42, 此例中不会发生改变
 7
 8    v1 = 0;
 9    auto j = f();
10    // j = 42
11}

引用捕获

当我们在lambda函数体内使用变量时,实际使用的是引用所绑定的对象

函数体内为v1的同名引用

 1void fcn2()
 2{
 3    size_t v1 = 42;
 4    auto f2 = [&v1] { return v1; }
 5    // 此时是引用捕获,数据成员保存v1的引用而非拷贝
 6
 7    v1 = 0;
 8    auto j = f2();
 9    // j = 0
10}

示例

biggies函数接受一个ostream的引用,用来输出数据;并接受一个字符作为分隔符

 1void biggies(vector<string> &words,
 2                vector<string>::size_type sz,
 3                ostream &os = cout, char c = ' ')
 4{
 5    // ...
 6
 7    for_each(words.begin(), words.end(),
 8            [&os, c](const string &s) { os << s << c; });
 9}
10
11// ostream对象不支持拷贝: 传递ostream对象的方法有两个, 通过引用和指针

如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda调用的时候是存在的


引用捕获的返回

函数可以返回lambda

如果该lambda使用了引用捕获, 其引用捕获的所有局部变量均无效


避免捕获指针和引用捕获


隐式捕获

除了显式给出lambda需要捕获的变量, lambda还支持批量捕获

-
= 对未显式给出名称的变量进行值捕获
& 对未显式给出名称的变量进行引用捕获

可以混用隐式捕获和显式捕获,对一部分变量采用值捕获,对另一部分变量采用引用捕获

混用隐式捕获和显式捕获时

  1. 捕获列表中的第一个元素必须是&或=, 即默认捕获方式
  2. 显式和隐式必须采用不同的捕获方式: 如果隐式捕获使用引用方式,那么显式捕获必须使用值捕获,不得在名字前加 &

捕获列表的6种形式

  1. 捕获列表为空

    []
  2. 显式捕获

    值捕获给出变量名, 引用捕获在变量名之前加上 &

    [identifier_list]
  3. 隐式值捕获

    值捕获所有可访问的局部变量

    [=]
  4. 隐式引用捕获

    引用捕获所有可访问的局部变量

    [&]
  5. 显式值捕获 + 隐式引用捕获: 给出需值捕获的变量名称, 引用捕获余下可访问的局部变量

    identifier_list里的变量均采用值捕获

    [&, identifier_list]
  6. 显式引用捕获 + 隐式值捕获: 给出需引用捕获的变量名称, 值捕获余下可访问的局部变量

    identifier_list里的变量均采用引用捕获,名字前一定有&

    [=, identifier_list]

隐式值捕获

1wc = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() >= sz; });

混用隐式捕获和显式捕获

1void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ')
2{
3    // os为隐式引用捕获;c为显式值捕获
4    for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c; });
5
6    // os为显式引用捕获;c为隐式值捕获
7    for_each(words.begin(), words.end(), [=, &os] { os << s << c; });
8}

定义lambda时, 编译器在背后所做的工作


定义lambda时

部分内容为猜测

  1. 值捕获的局部变量

    在类中定义了数据成员, 为局部变量的拷贝

  2. 引用捕获的局部变量

    书上说无需将其存储为数据成员 P508

    在类中定义了引用类型数据成员, 绑定局部变量对应的对象

  3. 为该类型重载了调用运算符

    调用运算符的重载版本是const成员函数

    lambda的参数为调用运算符的参数, lambda的函数体为调用运算符的实现

    函数体内使用的为自己的数据成员. 因为是const成员函数, 无法对值捕获的变量执行写操作

编译器根据lambda的定义生成了一个类型, 并创建了该类型的一个对象

创建该类对象时, 数据成员被初始化

建议减少捕获的数据量, 都是要占用内存的


向函数传递lambda时

同时定义了一个新类型与该类型的一个对象

传递的是编译器生成的类类型的未命名对象


使用auto定义一个用lambda初始化的变量时

定义了一个根据lambda生成的类型的对象

对对象使用调用运算符时, 才会执行lambda表达式的函数体


可变lambda

mutable

类的成员函数那里, 如果将数据成员声明为 mutable , 在const成员函数中也可对这些数据成员执行写操作

猜测: 书上已证实

  • lambda对应类型的调用运算符重载为const成员函数

猜测: 书上说调用运算符不再为const P508

如果将lambda声明为 muable , 编译器定义类型时, 会使用值捕获的局部变量初始化可变数据成员


声明lambda表达式时未使用mutable, 不可对值捕获的同名拷贝执行写操作

1size_t v1 = 42;
2auto f = [v1]() { return ++v1; };    // 报错
3v1 = 0;
4auto j = f();
error: cannot assign to a variable captured by copy in a non-mutable lambda

可以对引用捕获的同名引用执行写操作

引用不具有底层const

 1size_t v1 = 42;
 2
 3auto f = [&v1]() { return ++v1; };
 4
 5// v1 = 42
 6
 7auto j = f();
 8
 9// v1 = 43, j = 43
10
11v1 = 0; // 修改引用捕获的变量的值
12
13auto j2 = f();        // j2 = 1, v1 = 1

可以在const成员函数中对引用类型数据成员执行写操作

 1#include <iostream>
 2
 3using namespace std;
 4
 5struct test
 6{
 7    int &a;
 8    test(int &aa) : a(aa) {}
 9
10    void printA() { cout << a << endl; }
11    void increaseA() const { ++a; }
12};
13
14int main()
15{
16    int a = 5;
17    test t(a);
18
19    t.printA();
20    t.increaseA();
21    t.printA();
22    cout << a << endl;
23    return 0;
24}
25
26// 5 6 6

声明lambda表达式时使用mutable关键字, 可以对值捕获的同名拷贝执行写操作

mutable关键字在形参列表之后给出

 1void fcn3()
 2{
 3    size_t v1 = 42;
 4
 5    auto f = [v1]() mutable { return ++v1; };
 6
 7    // v1 = 42
 8
 9    auto j = f();
10
11    // j = 43,lambda对象中的v1为43
12
13    auto k = f();
14
15    // k = 44,lambda对象中的v1为44
16}

可变lambda省略参数列表

仅作了解

  1. lambda表达式支持省略参数列表

    没有问题
    1auto g = [] { return 42; };
  2. 使用mutable关键字时
    • 没有问题

      1auto f = [v1] () mutable { return ++v1; };
    • 有告警

      1auto f = [v1] mutable { return ++v1; };
      warning: lambda without a parameter clause is a C++2b extension

lambda使用尾置返回

  1. 省略返回类型时

    • 如果lambda表达式的函数体只包含单一return语句, 编译器根据返回值中推断出返回类型

      1// 工作正常,对每个元素取绝对值
      2
      3transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i < 0 ? -i : i; });
    • 如果lambda表达式包含return语句外的任何语句, 编译器认为其返回类型为void

      1// 编译错误:除return语句外还有其他语句,编译器认为该lambda返回类型为void,此时不能有返回值
      2
      3transform(vi,begin(), vi.end(), vi.begin(), [](int i) { if (i < 0) return -i; else return i; });
  2. 使用尾置返回

    1transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; });

创建新序列, 为给定序列的映射

transform

给出序列1的所有元素, 使用迭代器范围: [b, e)

给出指向序列2首元素的迭代器

给出一元谓词: 对序列1中的每个元素调用给定的一元谓词, 将返回值写入序列2

指向序列2首元素的迭代器可以是b