《Effective Modern C++》学习笔记 - Item 34: 倾向于使用lambda函数而非std::bind

  • 太长不看版:std::bind 是2005年TR1提案引入的特性,相比C++98的 std::bind1ststd::bind2nd 是一个大进步;但到了C++11时代,lambda 函数几乎可以代替它,仅有几种特定情况适合使用 std::bind。C++14消灭了这些情况,现在 lambda 函数已经完全是 std::bind 的上位替代

  • 本节通过几个例子来展示 lambda 相比 std::bind 的若干优势。我们假设你已经熟悉 std::bind 的基本使用。

  • lambda 的第一个优势是代码可读性。假设我们要写一个设置闹铃的函数,它有三个参数:开始响的时间,响铃声音的类型和响铃的持续时间。基本代码如下:

using Time = std::chrono::steady_clock::time_point;
using Duration = std::chrono::steady_clock::duration;
enum class Sound { Beep, Siren, Whistle };

// at time t, make sound s for duration d
void setAlarm(Time t, Sound s, Duration d);
  • 假设在程序某处我们已经确定了 Time 和 Duration 参数,需要包装一个只接受 Sound 参数的函数,使用 lambda 写法如下:
// 设置一小时后,响铃30秒,响铃声音类型是参数
// L for lambda
auto setSoundL = [](Sound s) {
	using namespace std::chrono;
	setAlarm(steady_clock::now() + hours(1),
			 s,
			 seconds(30));
};

//C++14简化版本,使用std::chrono中的字面值(1h,30s)
auto setSoundL = [](Sound s) {
	using namespace std::chrono;
	setAlarm(steady_clock::now() + 1h, s, 30s);
};
  • 使用 std::bind,可以写出一个“看似正确”的版本:
using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB = std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);
  • 阅读这段代码的人想知道 _1 的类型必须参考 setAlarm 的源码。调用 setSoundB 时的“体验”也是很糟糕的(下图为VS中调用两种函数的代码提示):
    在这里插入图片描述
    在这里插入图片描述

  • 更重要的是,使用 std::bind,上面的写法是不正确的!调用 setSoundLsteady_clock::now() 随之被调用,获取的是 setSoundL 被调用时点的时间,这是正确的。但是 setSoundB 的版本,steady_clock::now() 是在 std::bind 中被调用的,绑定的不是 setSoundB 而是 std::bind 被调用的时点

在这里插入图片描述

测试:笔者在 setAlarm 函数中输出了当前时间 t.time_since_epoch(),分别调用两次 setSoundL 和两次 setSoundB,中间各休眠1秒。可以看到两次 setSoundL 调用输出了不同结果,说明 t 是在调用时才被计算;而两次 setSoundB 调用输出结果相同,实际上都是调用 std::bind 的时间。

  • 要将 std::bind 参数的表达式延迟到调用时再评估,则需要再嵌套std::bind,具体写法如下:(注意:原书这里给出的代码是错误的!Scott Meyer在其个人网站上记录了这个问题(链接),这里直接展示正确写法)
// C++14中std::plus可以不写参数类型,如果是C++11应该写成std::plus<steady_clock::time_point>
auto setSoundB = std::bind(setAlarm,
						   std::bind(std::plus<>(), std::bind(steady_clock::now), 1h),
						   _1,
						   30s);

在这里插入图片描述

  • (作者表示如果到这里你还不觉得 lambda 版更好,那确实该去看看眼科了ww)

  • 如果 setAlarm 有重载版本,那么问题还会更多。假设有一个添加音量参数的重载:

enum class Volume { Normal, Loud, LoudPlusPlus };

void setAlarm(Time t, Sound s, Duration d, Volume v);

lambda 版继续正常运行,调用原来的三参数版本。std::bind 版会报编译错误,因为我们之前实际上是用函数名替代了调用的对象,当存在重载版本的多个函数时,编译器将无法进行自动推断,进而报错。这里的本质问题是C和C++中 function name 是可以用来替代 function pointer 的,但是当出现重载导致语义不清晰(ambiguous)时,替代就会失败。

  • 要解决这个问题,我们只能通过 static_cast 来明确 setAlarm 的类型:
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);		// 函数真实类型
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
						   std::bind(std::plus<>(), std::bind(steady_clock::now), 1h),
						   _1,
						   30s);
  • 以上思考其实会引出另一个问题:lambda 版在调用时,与一个普通的函数调用无异,编译器会按正常执行函数内联的优化;而 std::bind 版在调用时实际是通过一个函数指针进行调用,这意味着编译器更可能不会进行内联优化。因此,lambda 版的生成码在运行效率上可能也优于 std::bind 版。

  • std::bind 使用的占位符(_1, _2,...)看起来就像“魔法”,但其不清晰之处还不止于此。例如下面这段代码:

enum class CompLevel { Low, Normal, High }; // compression level
Widget compress(const Widget& w, 	// make compressed
				CompLevel lev); 	// copy of w

Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);

一个重要的问题是 w 是按值还是按引用存储在 std::bind 中的?如果是前者,那么我们在定义和调用 compressRateB 之间对 w 做的修改就不会体现在调用中,如果是后者则会。

答案是按值存储,因为 std::bind 总是拷贝它的参数。我们在 compressRateB 的定义中无法看出这一点,只能通过查询或记忆 std::bind 的原理获悉。而在 lambda 版本中,我们可以显式地控制:

auto compressRateL = 				// w is captured by
	[w](CompLevel lev) 				// value; lev is
 	{ return compress(w, lev); }; 	// passed by value
  • 在C++11中,的确还存在两个适合用 std::bind 的场景,其中一个是 Item 32 中讲述的移动捕获(这里不再重复),另一个是泛型。C++11中,lambda 函数的参数类型是确定的,如果需要使用泛型参数则可以借助 std::bind 完美转发其参数的特性实现,例如下面的代码:
class PolyWidget {
public:
	template<typename T>
	void operator()(const T& param);
...
};

PolyWidget pw;
auto boundPW = std::bind(pw, _1);
boundPW(1930); 		// pass int to PolyWidget::operator()
boundPW(nullptr); 	// pass nullptr to PolyWidget::operator()
boundPW("Rosebud"); // pass string literal to PolyWidget::operator()
  • C++14中,这两个case也不再成立:对于移动捕获,lambda 有了广义捕获;对于泛型,lambda 有了 auto 参数:
auto boundPW = [pw](const auto& param)	// C++14
			   { pw(param); };

总结

  1. 使用 lambda 函数相比 std::bind 代码可读性更好、表达能力更强、运行效率可能更高。
  2. C++11中,std::bind 还有实现移动捕获和泛型参数这两种适用作用;C++14中 lambda 也可以完美做到这些。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值