lambda捕获和返回
2023年12月24日 2024年1月4日
需要知道定义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还支持批量捕获
- | |
---|---|
= | 对未显式给出名称的变量进行值捕获 |
& | 对未显式给出名称的变量进行引用捕获 |
可以混用隐式捕获和显式捕获,对一部分变量采用值捕获,对另一部分变量采用引用捕获
混用隐式捕获和显式捕获时
- 捕获列表中的第一个元素必须是&或=, 即默认捕获方式
- 显式和隐式必须采用不同的捕获方式: 如果隐式捕获使用引用方式,那么显式捕获必须使用值捕获,不得在名字前加
&
捕获列表的6种形式
-
捕获列表为空
[]
-
显式捕获
值捕获给出变量名, 引用捕获在变量名之前加上&
[identifier_list]
-
隐式值捕获
值捕获所有可访问的局部变量[=]
-
隐式引用捕获
引用捕获所有可访问的局部变量[&]
-
显式值捕获 + 隐式引用捕获: 给出需值捕获的变量名称, 引用捕获余下可访问的局部变量
identifier_list里的变量均采用值捕获[&, identifier_list]
-
显式引用捕获 + 隐式值捕获: 给出需引用捕获的变量名称, 值捕获余下可访问的局部变量
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时
部分内容为猜测
-
值捕获的局部变量
在类中定义了数据成员, 为局部变量的拷贝 -
引用捕获的局部变量
书上说无需将其存储为数据成员 P508
在类中定义了引用类型数据成员, 绑定局部变量对应的对象 -
为该类型重载了调用运算符
调用运算符的重载版本是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省略参数列表
仅作了解
- lambda表达式支持省略参数列表
没有问题
1auto g = [] { return 42; };
- 使用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使用尾置返回
-
省略返回类型时
-
如果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; });
-
-
使用尾置返回
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