本文为《C++ Primer》的读书笔记
lambda 表达式
- 一个 lambda 表达式可以理解为一个未命名的内联函数
- lambda 必须使用尾置返回 来指定返回类型
- 可以忽略参数列表 (等价于指定一个空参数列表)
- 可以忽略返回类型。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型
- 如果函数体只是一个
return
语句, 则返回类型从返回的表达式的类型推断而来 - 否则,如果 lambda 的函数体包含任何单一
return
语句之外的内容, 且未指定返回类型, 则返回void
(即不能返回值)
- 如果函数体只是一个
- 必须永远包含捕获列表和函数体
- 不能有默认参数
// capture list (捕获列表)是一个 lambda 所在函数中定义的局部变量的列表
[capture list] (parameter list) -> return_type { function body }
// 空捕获列表表明此 lambda 不使用它所在函数中的任何局部变量
stable_sort(words.begin(), words.end(),
[] (const string &a, const string &b)
{ return a.size() < b.size(); });
捕获列表
- 一个 lambda 可以出现在一个函数中并使用其局部变量, 但它只能使用那些明确指明的变量
- 捕获列表 为一个 以逗号分隔的名字列表, 这些名字都是它所在函数中定义的
- 一个 lambda 通过将局部非
static
变量包含在其捕获列表中来指出将会使用这些变量,局部static
变量可以直接使用
void biggies(vector<string> &words, vector<string>::size_type sz)
{
// 获取一个迭代器, 指向第一个满足 size() >= sz的元素
auto wc = find_if(words.begin(), words.end(),
[sz] (const string &a)
{ return a.size() >= sz; });
}
lambda 捕获和返回
lambda 是函数对象
- 当定义一个 lambda 时, 编译器生成一个与 lambda 对应的新的(未命名的)函数对象。 即,当定义一个 lambda 时, 同时定义了一个新类型和该类型的一个对象
- 例如, 当使用
auto
定义一个用 lambda 初始化的变量或向函数传递一个 lambda 时, 定义了一个从 lambda 生成的类型的对象
- 例如, 当使用
// 获得第一个指向满足条件元素的迭代器, 该元素满足 size() > = sz
auto we = find_if(words.begin(), words.end(),
[sz] (const string &a) {return a.size() > = sz;});
// 其行为类似于下面这个类的一个未命名对象
class SizeComp {
SizeComp (size_t n): sz(n) { } // 该形参对应捕获的变量
// 该调用运算符的返回类型、形参和函数体都与 lambda 一致
// lambda 不加 mutable 修饰的话,重载的函数调用运算符为 const 成员函数
bool operator() (const string &s) const
{ return s.size() >= sz; }
private:
size_t sz; // 该数据成员对应通过值捕获的变量
};
auto we = find_if(words.begin(), words.end(), SizeComp(sz));
- 默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员。类似任何普通类的数据成员, lambda 的数据成员也在 lambda 对象创建时被初始化;类似参数传递, 变量的捕获方式也可以是值或引用。下表列出了几种不同的构造捕获列表的方式:
值捕获
- 采用值捕获的前提是变量可以拷贝
- 与参数不同, 被捕获的变量的值是在 lambda 创建时拷贝, 而不是调用时拷贝:
void fcn1()
{
size_t v1 = 42;
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j 为 42
}
引用捕获
- 引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量, 就必须确保被引用的对象在 lambda 执行的时候是存在的。lambda 捕获的都是局部变量, 这些变量在函数结束后就不复存在了。如果 lambda 可能在函数结束后执行, 捕获的引用指向的局部变量已经消失
- 因此,如果函数返回一个 lambda, 则 此 lambda 不能包含引用捕获
void fcn2()
{
size_t v1 = 42;
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2() ; // j 为 0
}
建议:尽量保持 lambda 的变量捕获简单化
一般来说,我们应该尽量减少捕获的数据量。而且、如果可能的话, 应该避免捕获指针或引用
隐式捕获
wc = find_if(words.begin(), words.end(),
[=] (const string &s)
{ return s.size() >= sz; });
- 如果我们希望对一部分变量采用值捕获, 对其他变量采用引用捕获, 可以混合使用隐式捕获和显式捕获:
可变 lambda
- 默认情况下, 由 lambda 产生的类当中的函数调用运算符是一个
const
成员函数,因此对于一个值被拷贝的变量,lambda 不会改变其值 - 如果我们希望能改变一个被捕获的变量的值, 就必须在参数列表之后、函数体及返回类型之前加上关键字
mutable
使调用运算符不是const
void fcn3()
{
size_t v1 = 42;
// f可以改变它所捕获的变量的值
auto f = [v1] () mutable { return ++v1; } ;
v1 = 0;
auto j = f(); // j 为 43
}
一个引用捕获的变量是否可以修改依赖于此引用指向的是一个
const
类型还是一个非const
类型
参数绑定
- 对于那种只在一两个地方使用的简单操作, lambda 表达式是最有用的;但如果我们需要在很多地方使用相同的操作, 通常应该定义一个函数, 而不是多次编写相同的 lambda 表达式
- 如果 lambda 的捕获列表为空, 通常可以用函数来代替它。但是, 用函数来替换捕获局部变量的 lambda 就不是那么容易了
标准库 bind
函数
#include <functional> // `bind` 函数和 `placeholders` 命名空间都定义在该头文件中
using namespace std::placeholders;
auto newCallable = bind(callable, arg_list);
- 可以将
bind
函数看作一个通用的函数适配器,它接受一个可调用对象, 生成一个新的可调用对象来 “适应” 原对象的参数列表- 其中,
arg_list
是一个逗号分隔的参数列表, 对应给定的callable
的参数。即, 当我们调用newCallable
时,newCallable
会调用callable
, 并传递给它arg_list
中的参数 arg_list
中的参数可能包含形如_n
的名字,其中n
是一个整数。这些参数是 “占位符”,表示newCallable
的参数,它们占据了传递给newCallable
的参数的 “位置” 。数值n
表示生成的可调用对象中参数的位置:_1
为newCallable
的第一个参数,_2
为第二个参数, 依此类推- 名字
_n
都定义在命名空间placeholders
中,而这个命名空间本身定义在std
命名空间中
- 名字
- 其中,
auto wc = find_if(words.begin(), words.end(),
[sz] (const string &a)
{ return s.size() >= sz; });
// 等价版本;check_size 接受两个参数,第一个参数为 str,第二个参数为长度
// 此 `bind` 调用生成一个可调用对象,将 `checksize` 的第二个参数绑定到 `sz` 的值
// 当 `find_if` 对 `words` 中的 `string` 调用这个对象时, 这些对象会调用`check_size`, 将给定的 `string` 和 `sz` 传递给它
auto wc = find_if(words.begin(), words.end(),
bind(check_size, _1, sz));
bind
甚至可以绑定成员函数的参数
class GameLevel {
public:
float health(const int&) const;
...
};
GameLevel currentLevel;
bind(&GameLevel::health, currentLevel, _1);
- 更一般的, 可以用
bind
绑定给定可调用对象中的参数或重新安排其顺序
// 假定 f 是一个可调用对象, 它有 5 个参数
// g 是一个有两个参数的可调用对象
// f 的第 1、2、4 个参数分别被绑定到给定的值 a、 b 和 c 上
// 调用 g 时, 其第一个参数将被传递给 f 作为最后一个参数, 第二个参数将被传递给 f 作为第三个参数
auto g = bind(f, a, b, _2, c, _1);
标准库 ref
/ cref
函数
#include <functional>
- 默认情况下,
bind
的那些不是占位符的参数被拷贝到bind
返回的可调用对象中。但是, 与 lambda 类似, 有时对有些绑定的参数我们希望以引用方式传递, 或是要绑定参数的类型无法拷贝
// os是一个局部变量, 引用一个输出流,该局部变量无法被拷贝
// c是一个局部变量, 类型为char
for_each(words.begin(), words.end(),
[&os, c] (const string &s) { os << s << c; });
// 错误: 不能拷贝os
for_each(words.begin(), words.end(), bind(print, os, _1, ' '));
// 必须使用标准库 `ref` 函数
for_each(words.begin(), words.end(),
bind(print, ref(os), 1, ' '));