34.尽量以lambda取代std::bind

前言

 
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
4
using 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
4
auto 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
5
auto 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
7
using 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
6
using 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
2
enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);

lambda运行得照样很流畅,因为它将直接调用setALarm的三参数版本:
1
2
3
4
auto 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
5
using 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
4
using 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
3
auto 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
2
enum class CompLevel { Low, Normal, High }; // compression level
Widget compress(const Widget& w, CompLevel lev); // make compressed copy of w

更进一步地,我们希望创建一个函数对象,它允许我们指定压缩品质,std::bind有实现如下:
1
2
3
Widget 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中它存在两点用武之地:

  1. 移动捕获
    (详见Item32)
  2. 多态函数对象
    因为bind对象的operator()使用完美转发,所以它可以接受任何类型的参数,因此当你想要讲一个对象绑定至模板函数operator()将非常有用,例如以下实例:
    1
    2
    3
    4
    5
    6
    class PolyWidget {
    public:
    template<typename T>
    void operator()(const T& param);

    };
    std::bind可以用如下形式绑定一个PolyWidget:
    1
    2
    PolyWidget pw;
    auto boundPW = std::bind(pw, _1);
    boundPW则可以通过多种参数完成调用:
    1
    2
    3
    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来解决:
    1
    auto boundPW = [pw](const auto& param){ pw(param); };

总结

  1. lambda在可读性与效率上均优于std::bind。
  2. C++11中可以利用std::bind实现移动捕获与将对象绑定至模板化operator()。