前言
std::bind是C++98中std::bind1st和std::bind2nd的C++11继承者,但C++11引入的lambda几乎全面优于std::bind,并且C++14中lambda得到了更进一步地加强。
问题实例
在Item32中,我们曾经提及std:bind返回一个bind对象。我们可以认为,lambda优于bind的最主要原因在于lambda具备更高的可读性,下文中的实例将证明这一点。
假定当前有一个声音报警函数:1
2
3
4using Time = std::chrono::steady_clock::time_point;
enum class Sound { Beep, Siren, Whistle };
using Duration = std::chrono::steady_clock::duration;
void setAlarm(Time t, Sound s, Duration d);
进一步地,我们假定警报将在设定完成后经过1小时触发,并且持续30s。目前尚未明确的是警报选用何种声音,因此我们可以编写一个lambda来完成设定:1
2
3
4auto setSoundL =[](Sound s){
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1),s,seconds(30));
};
C++14支持时间后缀(s、ms、h等等),它们位于命名空间std::literals之中,因此上述代码可以进一步被简化为:1
2
3
4
5auto setSoundL =[](Sound s){
using namespace std::chrono;
using namespace std::literals;
setAlarm(steady_clock::now() + 1h,s,30s);
}
有关bind的第一次尝试
我们试图用std::bind完成上述功能:1
2
3
4
5
6
7using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders; // needed for use of "_1"
auto setSoundB = std::bind(setAlarm,
steady_clock::now() + 1h, // incorrect! see below
_1,
30s);
这段代码的读者只需知道调用setSoundB是将会触发setAlarm的调用。_1
的含义是:将setSoundB的第一个参数作为setAlarm的第二个参数。在调用std::bind时并不能明确此参数的类型,因此读者必须查阅setAlarm声明以确认要传递给setSoundB的参数类型。
上述代码并不正确。在lambda中,表达式“steady_clock::now()+ 1h”是setAlarm的参数,调用setAlarm时将对其进行求值(evaluate),因此完成在setAlarm后1小时触发报警。 但在std :: bind调用中,“steady_clock::now()+ 1h”是传递给std::bind的参数,而不是传给setAlarm,这意味着将在调用std::bind时完成表达式求值,也就是在调用std::bind一小时后触发警报。
有关bind的第二次尝试
修复上述问题的关键在于令std::bind明确在调用setAlarm前推迟对表达式的求值,为此我们需要引入第二个bind:1
auto setSoundB =std::bind(setAlarm,std::bind(std::plus<>(), steady_clock::now(), 1h),_1,30s);
如果熟悉C++98中的std::plus模板的话,你可能会惊讶地发现当前模板并未指定模板参数,即我们撰写的是“std::plus<>”而非 “std::plus<type>”。原因在于C++14中通常可以省略标准运算符模板的模板类型参数,因此不需要在此处提供它。 C+ 11没有提供这样的功能,因此与lambda等效的C++11 std::bind需要写为:1
2
3
4
5
6using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB =std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(),steady_clock::now(),hours(1)),
_1,
seconds(30));
引入重载后的问题实例
当setAlarm添加了重载函数后,问题再次产生了变化。假定当前我们除了可以设定声音类型外,还可以设定声音大小:1
2enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);
lambda运行得照样很流畅,因为它将直接调用setALarm的三参数版本:1
2
3
4auto setSoundL = [](Sound s){
using namespace std::chrono;
setAlarm(steady_clock::now() + 1h,s,30s); // calls 3-arg version of setAlarm
};
但bind则无法通过编译,原因在于它并不能确定调用哪个setAlarm:1
2// error! which setAlarm?
auto setSoundB =std::bind(setAlarm,std::bind(std::plus<>(), steady_clock::now(), 1h),_1,30s);
为了解决这一问题,必须将setAlarm转换为正确的函数指针类型:1
2
3
4
5using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
std::bind(std::plus<>(),steady_clock::now(),1h),
_1,
30s)
但这会带来lambda和std::bind之间的另一大区别。在setSoundL的operator()(即lambda所生成的closure class的operator())中,对setAlarm的调用是一个普通的函数调用,因此可以通过编译器完成内联:1
setSoundL(Sound::Siren); // body of setAlarm may well be inlined here
但在std::bind的调用中,编译器将函数指针传递给setAlarm,这意味着在setSoundB的operator()内(即bind对象的operator()),对setAlarm的调用通过函数指针完成。编译器不太可能通过函数指针内联函数调用,这意味着std::bind的内联程度往往低于lambda版本:1
setSoundB(Sound::Siren); // body of setAlarm is less likely to be inlined here
因此,使用lambda在效率上亦可能优于bind。
问题实例二
setAlarm示例仅涉及一个简单的函数调用,lambda在复杂事物的处理中将更加大放异彩。考虑如下所示的C++14 lambda,它返回其参数是否在最小值(lowVal)和最大值(highVal)之间,其中lowVal和highVal是局部变量:1
auto betweenL =[lowVal, highVal](const auto& val){ return lowVal <= val && val <= highVal; };
C++11不支持在lambda中使用auto,因此其形式如下:1
auto betweenL = [lowVal, highVal](int val){ return lowVal <= val && val <= highVal; };
std::bind也能完成同样的工作,只是过于晦涩:1
2
3
4using namespace std::placeholders;
auto betweenB =std::bind(std::logical_and<>(),// C++14
std::bind(std::less_equal<>(), lowVal, _1),
std::bind(std::less_equal<>(), _1, highVal));
C++11所需要的代码量更多:1
2
3auto betweenB =std::bind(std::logical_and<bool>(),
std::bind(std::less_equal<int>(), lowVal, _1),
std::bind(std::less_equal<int>(), _1, highVal));
显然,lambda版本代码量更少,且具有更高的可读性与可维护性。
std::bind的些许晦涩之处
前文中出现的_1
可能会令某些从未接触过std::bind的读者大感神奇,但std::bind的晦涩之处不止如此。假定当前我们有一个函数来创建Widget的压缩副本:1
2enum class CompLevel { Low, Normal, High }; // compression level
Widget compress(const Widget& w, CompLevel lev); // make compressed copy of w
更进一步地,我们希望创建一个函数对象,它允许我们指定压缩品质,std::bind有实现如下:1
2
3Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);
显然,当我们将w传递给std::bind时,必须将其存储于bind对象内部以便于后期的压缩函数调用。但本次实现是以refernece还是value方式存储?若以引用存储,w在调用bind对象之前发生的改变将反应于compress之中,否则不然。答案是std::bind按值存储,std::bind表达式并不表明其存储方式(这意味着你只能硬生生记下来),这一点与lambda的显式声明并不相同:1
2// w is captured by value; lev is passed by value
auto compressRateL =[w](CompLevel lev){ return compress(w, lev); };
以何种方式将参数传递给lambda也十分明确:1
compressRateL(CompLevel::High); // arg is passed by value
但std::bind则不然:1
compressRateB(CompLevel::High); // how is arg passed?
同样,唯一知道传入参数方式的方法是记住std::bind的工作原理。(答案是传递给bind对象的所有参数都是by-reference,因为这些对象的operator()使用完美转发。)
std::bind的可用之处
在C++14中std::bind全面落后于lambda,但在C++11中它存在两点用武之地:
- 移动捕获
(详见Item32) - 多态函数对象
因为bind对象的operator()使用完美转发,所以它可以接受任何类型的参数,因此当你想要讲一个对象绑定至模板函数operator()将非常有用,例如以下实例:std::bind可以用如下形式绑定一个PolyWidget:1
2
3
4
5
6class PolyWidget {
public:
template<typename T>
void operator()(const T& param);
…
};boundPW则可以通过多种参数完成调用:1
2PolyWidget pw;
auto boundPW = std::bind(pw, _1);C++11无法模拟出上述情形,C++14可以通过auto来解决:1
2
3boundPW(1930); // pass int to PolyWidget::operator()
boundPW(nullptr); // pass nullptr to PolyWidget::operator()
boundPW("Rosebud"); // pass string literal to PolyWidget::operator()1
auto boundPW = [pw](const auto& param){ pw(param); };
总结
- lambda在可读性与效率上均优于std::bind。
- C++11中可以利用std::bind实现移动捕获与将对象绑定至模板化operator()。