C++基础(十二):lambda 表达式、参数绑定 (用普通函数代替 lambda)

本文为《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 表示生成的可调用对象中参数的位置: _1newCallable 的第一个参数,_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, ' '));
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值