Effective Modern C++ Item 34 的学习和解读。
C++11 的 std::bind 是对 C++98 std:bind1st 和 std::bind2nd 的继承,它在 2005 年以 TR1 文档形式非正式地成为标准库的一部分。因为,许多 C++ 程序员可能有十几年的 std::bind 使用经验,现在告诉他放弃使用 std::bind,多少可能有些不情愿。但是,本 Item 会告诉你使用 lambda 替代 std::bind 将是个更好的选择。
对于 C++11,除了个别边缘 case,lambda 表达式要比 std::bind 更有优势。而对于 C++14,lambda 则可以完全替代 std::bind。
lambda 第一个优势是代码的可读性更强。例如,我们有一个设置声音报警的函数:
// typedef for a point in time (see Item 9 for syntax)
using Time = std::chrono::steady_clock::time_point;
// see Item 10 for "enum class"
enum class Sound { Beep, Siren, Whistle };
// typedef for a length of time
using Duration = std::chrono::steady_clock::duration;
// at time t, make sound s for duration d
void setAlarm(Time t, Sound s, Duration d);
如果我们想在设置声音报警后 1h,关闭报警,并持续 30s。使用 lambda 表达式修正 setAlarm
,可以实现如下:
// setSoundL ("L" for "lambda") is a function object allowing a
// sound to be specified for a 30-sec alarm to go off an hour
// after it's set
auto setSoundL =
[](Sound s)
{
// make std::chrono components available w/o qualification
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1), // alarm to go off
s, // in an hour for
seconds(30)); // 30 seconds
};
上述代码逻辑非常清楚。如果使用 C++14 字面值 std::literals
改写上面代码,可以更加简洁:
auto setSoundL =
[](Sound s)
{
using namespace std::chrono;
using namespace std::literals; // for C++14 suffixes
setAlarm(steady_clock::now() + 1h, // C++14, but
s, // same meaning
30s); // as above
};
如果使用 std::bind 直接替换 lambda 表达式,可以改写成如下:
using namespace std::chrono; // as above
using namespace std::literals;
using namespace std::placeholders; // needed for use of "_1"
auto setSoundB = // "B" for "bind"
std::bind(setAlarm,
steady_clock::now() + 1h, // incorrect! see below
_1,
30s);
首先,相较于 lambda 版本,使用 std::bind,函数调用和传参不那么明显。并且这里还有一个占位符 “_1”,使用 setSoundB
时候,你需要查阅 setAlarm
的函数申明,才知道这里的占位符的传参类型。
最重要的是这里的代码逻辑有问题。显然,我们期望的是在调用 setAlarm
时候计算表达式 steady_clock::now() + 1h
的值。但是,使用 std::bind 的时候,表达式 steady_clock::now() + 1h
是传递给 std::bind 而不是 setAlarm
,这意味着,在调用 std::bind 的时候,表达式的值就被计算出来,然后保存在绑定对象内部。这就导致和在调用 setAlarm
时候计算表达式的期望不一致。可以再使用一个 std::bind 封装该表达式以延迟到 setAlarm
调用的时候才计算:
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<>(), steady_clock::now(), 1h), // C++14
_1,
30s);
注意到 std::plus<>
缺省了类型参数,这是 C++14 的新特性,如果是 C++11,则需要指定类型:
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(), // C++11
steady_clock::now(),
hours(1)),
_1,
seconds(30));
如果 setAlarm
增加一个重载版本:
enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);
先前 lambda 版本代码依然可以正常工作。但是,std::bind 将会产生编译报错。因为编译器无法确认传递哪个版本的 setAlarm
。需要将 setAlarm
转换为合适的函数指针:
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = // now
std::bind(static_cast<SetAlarm3ParamType>(setAlarm), // okay
std::bind(std::plus<>(),
steady_clock::now(),
1h),
_1,
30s);
但是,这又引入了 std::bind 和 lambda 二者的不同。setSoundL
使用正常的函数调用来调用 setAlarm
,编译器可以选择使用内联。
setSoundL(Sound::Siren); // body of setAlarm may
// well be inlined here
但是 std::bind 不可以,setSoundB
使用函数指针调用调用 setAlarm
,这是运行期的行为,无法被内联。
setSoundB(Sound::Siren); // body of setAlarm is less
// likely to be inlined here
这就是使用 lambda 的第二个优势:代码的性能可能会更好。
使用 lambda 的第三个优势是代码更容易理解。看下面的例子:
enum class CompLevel { Low, Normal, High }; // compression level
Widget compress(const Widget& w, // make compressed
CompLevel lev); // copy of w
假设我们想创建一个函数对象,用来指定特定 Widget
的压缩等级。使用 std::bind 创建函数对象:
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);
传递 w
给 std::bind,那么 w
如何存放在 compressRateB
内部的呢?是传值还是传引用?如果 w
在调用 std::bind
和 compressRateB
之间发生改变,传引用的方式将导致结果的不同。
std::bind 默认是拷贝它的参数到绑定对象内,用户可以使用 std::ref
指定传引用:
auto compressRateB = std::bind(compress, std::ref(w), _1);
这就需要你了解 std::bind 实现机制。但对于 lambda 的实现版本,w
是值捕获还是引用捕获非常明显:
auto compressRateL = // w is captured by
[w](CompLevel lev) // value; lev is
{ return compress(w, lev); }; // passed by value
同样明显的是参数如何传递给 lambda 的。这里,很明显 lev
是值传递:
compressRateL(CompLevel::High); // arg is passed
// by value
但是,std::bind 的绑定对象的调用,参数是如何传递的?
compressRateB(CompLevel::High); // how is arg
// passed?
答案是引用传递,这就需要你了解 std::bind 的工作机制:std::bind 绑定对象的函数调用使用了完美转发机制。
通过上述比较我们可以看到,相较于使用 std::bind,使用 lambda 表达式的代码可读性更强、更容易理解、性能可能更好。对于 C++14,你没有理由不选择使用 lambda。对于 C++11,只有两种场景,std::bind 可以弥补 lambda 的不足:
第一:移动捕获。C++14 的初始化捕获模式支持移动捕获。C++11 的 lambda 不支持移动捕获,可以使用 std::bind 模拟来间接实现,参见 Item32 。
第二:多态函数对象。C++14 支持 auto 参数类型,也即通用 lambda,参见 Item33 。但是 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++11 做不到,C++14 则很容易:
auto boundPW = [pw](const auto& param) // C++14
{ pw(param); };
最后总结下: