std::bind是C++98中std::bind1st和std::bind2nd的后继特性,但是作为一种非标准特性而言,std::bind在2005年就已经是标准库的组成部分了。正是在那时,标准委员会接受了名称TR1的文档,里面就包含了std::bind的规格(在TR1中,bind位于不同的名字空间,所以是std::tr1::bind而非std::bind,还有一些借口细节与现在有所不同)。
这样的历史意味着,有些开发者已经有了十多年的std::bind开发经验。如果你是他们中的一员,那你可能不太情愿放弃这么一个运作良好的工具。这可以理解。但是对于这个特定的情况,改变是有收益的。
因为在C++11中,相对于bind,lambda式几乎总会是更好的选择。到了C++14,lambda不仅优势变大,更成为了不二之选。
该条款假设你熟悉std::bind。如果你还不熟悉,那么在继续阅读前,需要先熟悉他们。
这里和Item 32一样,std::bind返回的函数对象叫做绑定对象。
来个例子:
// 表示时刻的型别typedef(语法参见Item 9) using Time = std::chrono::steady_clock::time_point; // 关于“enum class”,参见Item 10 enum class Sound {Beep, Siren, Whistle}; // 表示时长的型别typedef using Duration = std::chrono::steady_clock::duration; // 在时刻t,发出声音s,持续时长d void setAlarm(Time t, Sound s, Duration d);
这里进一步假设,在程序的某处,我们想要设置在一小时之后,发出警报并持续30秒。警报的具体声音,却尚未确定。
这么一来,我们可以撰写一个lambda式,修改setAlarm的接口,这个新的接口只需要指定声音即可:
// setSoundL("L"表示Lambda)是个函数对象 // 它接受指定一个声音 // 该声音将在设定后一小时发出,并持续30秒 auto setSoundL = [](Sound s) { //使std::chrono组件不加限定饰词即可使用 using namespace std::chrono; setAlarm(steady_clock::now() + hours(1), //报警发出的时刻为1小时后 s, seconds(30)); //持续30秒 };
如果是在C++14里,那么上述的代码可以写的更具可读性:
auto setSoundL = [](Sound s) { using namespace std::chrono; using namespace std::literals; //C++14支持实现后缀 setAlarm(steady_clock::now() + 1h, //这里直接用1h表示 s, 30s); //30s表示 };
那么如果用std::bind来写会是什么样呢?下面的代码其实包含了一个错误,我们后续会修复它,先看看代码:
using namespace std::chrono; using namespace std::literals; using namespace std::placeholders; //这里是因为要用bind对应的占位符 auto setSoundB = //B表示bind std::bind(setAlarm, steady_clock::now() + 1h, //这里有个错误! _1, 30s);
对于初学者而言,这种“_1"占位符简直好比天书,但即使是行家,也许脑补出从阿占位符中数字到它在std::bind形参列表位置的映射关系,才能理解,在调用setSoundB时传入的第一个参数,会作为第二个实参传递给setAlarm。而且你还不知道这个实参的类型是什么,需要查看setAlarm的声明。
接下来看看错误在哪里:
在lambda式中,表达式steady_clock::now() + 1h是setAlarm的实参之一,这一点清清楚楚,该表达式会在setAlarm被调用的时刻评估求值。这样做合情合理,我们就是想要在setAlarm被调用后的一个小时之后启动报警。
在std::bind的调用中,steady_clock::now() + 1h作为实参被传递给了std::bind,而非setAlarm。意味着表达式评估求值的时刻是在调用std::bind的时刻,并且求得的时间结果会被存储在结果绑定对象中。最终导致的结果是,报警被设定的启动时刻是在调用std::bind的时刻之后的一个小时,而并非调用setAlarm的时刻之后的一个小时!
想解决这个问题,就像需要让std::bind来延迟表达式的评估求值到调用setAlarm的时候,而实现这一点的途径是在原来的std::bind上再嵌套一个std::bind.
//C++14,标准运算符模板的模板型别实参大多数情况可以不写 auto setSoundB = std::bind(setAlarm, std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s); //C++11 auto setSoundB = std::bind(setAlarm, std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)), _1, seconds(30));
事情到这里,已经很明显的显示出lambda的优势了,可读性强了不少。
一旦对setAlarm实施了重载,新的问题就会马上出现。
假如有个重载版本会接受第四个形参,用以指定报警的音量:
enum class Volume {Normal, Loud, LoudPlusPlus}; void setAlarm(Time t, Sound s, Duration d, Volume v);
之前的lambda表示没问题,可以正常调用3形参版本重载。
但是对std::bind的调用,就无法通过编译了:
auto setSoundB = std::bind(setAlarm, //错误!这里不知道如何选择了 std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)), _1, seconds(30));
错误的根因在于编译器只拿到一个函数名,但是这个函数本身是多义的。
如果还是要让std::bind能运作,那么要写成这样:
using SetALarm3ParamType = void(*)(Time t, Sound s, Duration d); auto setSoundB = std::bind(static_cast<SetALarm3ParamType>(setAlarm), std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)), _1, seconds(30));
但即便你觉得这样写好,多写几行没什么问题,但这么写还是带来了更大的问题:
在SetSoundL的函数中,调用setAlarm采用的是常规的函数唤起方式,这么一来,编译器就可以用惯常的手法将其内联:
setSoundL(Sound::Siren); //这里,setAlarm的函数体大可以被内联
可是,std::bind的调用传递了一个指涉到setAlarm的函数指针,而那就意味着setAlarm的调用是通过函数指针发生的。编译器不太会内联掉通过函数指针发起的函数调用,所以后一种写法被内联的几率大大降低。所以lambda式形式的调用被优化的可能性更高。
而不仅如此,上面的例子仅仅涉及一个函数调用,如果你想做的事情比这更复杂,使用lambda式的好处会急剧增大。
看下面一个例子,需求是判断某个参数是否在最大值和最小值之间:
//C++14 auto betweenL = [lowVal, highVal] (const auto& val) { return lowVal <= val && val <= highVal;};
//std::bind C++14 using namespace std::placeholders; auto betweenB = std::bind(std::logical_and<>(), std::bind(std::less_equal<>(), lowVal, _1), std::bind(std::less_equal<>(), _1, highVal));
C++11还不支持模板泛型自动推导,还要改成这样:
//std::bind C++11 using namespace std::placeholders; auto betweenB = std::bind(std::logical_and<bool>(), std::bind(std::less_equal<int>(), lowVal, _1), std::bind(std::less_equal<int>(), _1, highVal));
这么一对比,已经太明显不过了。
试想这样一个场景,需要压缩一个类,然后返回这个类的副本:
enum class CompLevel { Low, Normal, High}; Widget compress(const Widget& w, CompLeve lev)l
然后写了个函数对象包装一下:
Widget w; using namespace std::placeholders; auto compressRateB = std::bind(compress, w, _1);
但是这里w是按值传递的还是按引用传递的,就让人很迷惑了。(这里有个前提,std::bind默认就是按值传递的,如果要用按引用,要显示写成:
auto compressRateB = std::bind(compress, std::ref(w), _1);
而lambda就很明显是按值:
auto compressRateL = [w](CompLeve lev) { return compress(w, lev);};
不仅仅是这里声明和定义的地方让人迷惑,调用的形式也让人不清不楚:
//Lambda式 compresssRateL(CompLevel::High); //实参按值传递 //bind式 compresssRateB(CompLevel::High); //这里是按照什么呢?
答案又会让你出乎意料,而且还是死记硬背没有原因的:
std::bind的工作原理,绑定对象的所有实参都是按引用传递的,因为此种对象的函数调用运算符利用了完美转发。
C++14里已经可以忘记std::bind了。
C++11里还有两种受限场合可以使用:
移动捕获:C++11语言不支持初始化捕获,只能用bind来模拟,详见Item 32
多态函数对象: 因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何型别的实参(除了在Item 30中提到的受限情况外)。这个特点再你想要绑定的对象具有一个函数调用运算符模板是,是有利用价值的。
例如:
class PolyWidget { public: template<typename T> void operator()(const T& param); ... };
这里来个bind:
PolyWidget pw; auto boundPw = std::bind(pw, _1);
这样一来,boundPw就可以通过任意性别的实参加以调用:
boundPw(1995); boundPw(nullptr); boundPw("Adam Xiao");
在C++11中的lambda式是办不到的,因为不支持泛型,但是C++14里依旧可以做到
//C++14 auto boundPw = [pw](const auto& param) {pw(param);};
lambda式比起使用std::bind而言,可读性更好,表达力更强,可能运行效率更高。
仅在C++11中,std::bind在实现移动捕获,或是多态函数对象的时候,还有余热可以发挥。