-
太长不看版:
std::bind
是2005年TR1提案引入的特性,相比C++98的std::bind1st
和std::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
,上面的写法是不正确的!调用setSoundL
,steady_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); };
总结
- 使用 lambda 函数相比
std::bind
代码可读性更好、表达能力更强、运行效率可能更高。 - C++11中,
std::bind
还有实现移动捕获和泛型参数这两种适用作用;C++14中 lambda 也可以完美做到这些。