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中,相对于std::bind,lambda几乎总是更好的选择,到了C++14,lambda不仅是优势变强,简直已成为不二之选。
该条款假设你熟悉std::bind,如果你还不熟悉,那么再继续阅读之前,还是需要建立一个基本认识。这种认识在任何情况下都是值得的,因为你并不会知道,在哪个时刻,就会在你需要阅读或维护的代码中遭遇std::bind.
和条款32一样,我称std::bind返回的函数对象为绑定对象。
之所以说优先选用lambda式,而非std::bind,最主要原因是lambda式具备更高的可读性。举个例子,假设我们有个函数用来设置声音警报:
//表示时刻的型别typedef
using Time = std::chrono::steady_clock::time_point;//关于 enum class
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")是个函数对象,
//它接受指定一个声音
//该声音将在设定后1小时发出,并持续30秒
auto setSoundL = [](Sound s)
{//使std::chrono组件不加限定饰词即可用using namespace std::chrono;setAlarm(steady_clock::now() + hours(1), //警报发出时刻为1小时后s,seconds(30)); //持续30秒
};
我将lambda式里对setAlarm的调用突显了出来,这是个观感无奇的函数调用,就算没有什么lambda经验的读者都能看的出来,传递给lambda的形参会作为实参传递给setAlarm.
我们可以利用C++14所提供的秒(s),毫秒(ms)和小时(h)等标准后缀来简化上述代码,这一点建立在C++11用户定义字面量这项能力的支持之上。这些后缀在std::literals名字空间里实现,所以上面的代码可以重写如下:
//setSoundL("L"表示"lambda")是个函数对象,
//它接受指定一个声音
//该声音将在设定后1小时发出,并持续30秒
auto setSoundL = [](Sound s)
{using namespace std::chrono;using namespace std::literals; //汇入C++14实现的后缀setAlarm(steady_clock::now() + 1h, //C++14s, //但和上一段代码30s); //意义相同
};
下面的代码就是我们撰写对应的std::bind调用语句的首次尝试。这段代码里面包含一个错误,我们过会儿来修复它,修正后的代码会复杂很多。但是,即使是这个简化的版本可以让我们看到一些重要议题:
using namespace std::chrono;
using namespace std::literals;using namespace std::placeholders; //本句是因为需要使用"_1"auto setSoundB = std::bind(setAlarm, steady_clock::now() + 1h,_1,30s);
我也想和在lambda式里一样地把setAlarm的调用在这里突显出来,可惜这里没有调用可供我标出突显。在读这段代码时,只需了解,在调用setSoundB时,会使用在调用std::bind时指定的时刻和时长来唤起setAlarm.固然对于初学者而言,占位符"_1"简直好比天书,但即使是行家也需要脑补出从占位符中数字到它在std::bind形参列表位置的映射关系,才能理解在调用setSoundB时传入的第一个实参,会作为第二个实参传递给setAlarm.该实参的型别在std::bind的调用过程中未加识别,所以你还需要去咨询setAlarm的声明方能决定应该传递何种型别的实参给到setSoundB.
但是,正如我前面提到的,这段代码不甚正确。在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的调用:
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中则没有这样的特性,所以在C++11中,欲使用std::bind撰写与lambda式等价的代码,就只能像下面这样:
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));
如果到了这个份上,你还是看不出lambda式的实现版本更有吸引力的话,那你真的要去检查视力了。
一旦对setAlarm实施了重载,新的问题就会马上浮现,加入有个重载版本会接受第四个形参,用以指定警报的音量:
enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);
之前那个Lambda式会一如既往地运作如仪,因为重载决议会选择那个三形参版本的setAlarm:
auto setSoundL = [](Sound s){using namespace std::chrono;setAlarm(steady_clock::now() + 1h, //没问题,调用的是s, //三形参版本的30s); //setAlarm};
不过,对std::bind的调用,现在可就无法通过编译了:
auto setSoundB = std::bind(setAlarm,std::bind(std::plus<>(), steady_clock::now(),hours(1)),_1,30s);
问题在于,编译器无法确定应该将哪个setAlarm版本传递给std::bind。它拿到的所有信息就只有一个函数名,而仅函数名本身是多义的。
为使得std::bind的调用能够通过编译,setAlarm必须强制转型到适当的函数指针型别:
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的函数调用运算符中(即,lambda式所对应的闭包类的函数调用运算符中)调用setAlarm陈代勇的是常规的函数唤起方式,这么一来,编译器就可以用惯常的手法将其内联:
setSoundL(Sound::Siren); //在这里,setAlarm的函数体大可以被内联
可是,std::bind的调用传递了一个指涉到setAlarm的函数指针,而那就意味着在setSoundB的函数调用运算符中(即,绑定对象的函数调用运算符中),setAlarm的调用是通过函数指针发生的。由于编译器不太会内联掉通过函数指针发起的函数调用,那也就意味着通过setSoundB调用setAlarm而被完全内联的几率,比起通过setSoundL调用setAlarm要低:
setSoundB(Sound::Siren); //在这里,setAlarm的函数体被内联的可能性不大
综上所述,使用lambda式就有可能会生成比使用std::bind运行更快的代码。
在setAlarm一例中,仅仅涉及了一个函数的调用而已。只要你想要的事情比这更复杂,使用lambda式的好处更会急剧扩大。例如,考虑下面这个C++14中的lambda式,它返回的是其实参是否在极小值(lowVal)和极大值(highVal)之间, 而lowVal和highVal都是局部变量:
auto betweenL = [lowVal, highVal](const auto& val){ return lowVal <= val && val <= hihgVal; };
std::bind也可以表达同样的意义,不过,要让它正常运作,必须用很晦涩的方式来构造代码:
using namespace std::placeholders; //同前auto betweenB =std::bind(std::logical_and<>(), //C++14std::bind(std::less_equal<>(), lowVal, _1),std::bind(std::less_equal<>(), _1, highVal));
如果是C++11,还必须要指定待比较物的型别,所以std::bind的调用会长成这样:
using namespace std::placeholders; //同前auto betweenB =std::bind(std::logical_and<bool>(), //C++11std::bind(std::less_equal<int>(), lowVal, _1),std::bind(std::less_equal<int>(), _1, highVal));
当然了,如果使用了C++11,就不能在Lambda式中使用auto型别的形参,所以也必须固化到一个型别才行:
auto betweenL =[lowVal, highVal](int val){ reutnr lowVal <= val && val <= highVal; };
不管是用C++11还是C++14,我希望大家都能认同,lambda式的版本不仅更加短小,还更易于理解和维护。
在前面,我曾提到,对于std::bind了无经验的程序员会感觉占位符(_1,_2等)看起来像天书一样,不过,可不仅仅只有占位符的行为如此诘屈聱牙。假设我们有一个函数用来制作Widget型别对象的压缩副本,
enum class CompLevel {Low, Normal, High}; //压缩等级Widget compress(const Widget& w, CompLevel lev); //制作w的压缩副本
然后我们想要创建一个函数对象,这样就可以指定特定的Widget型别对象w的压缩级别了。运用std::bind,可以创建出这么一个对象:
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);
这里,当我们把w传递给std::bind时,然后加以存储,以供未来让compress调用时使用,它存储的位置是在对象compressRateB内,但它以哪一种方式存储的:按值,还是按引用呢?这两者是泾渭分明的,因为如果w在对std::bind的调用动作与对compressRateB调用动作之间被修改了,如果采用按引用方式存储,那么存储起来的w的值也会随之修改,而如果采用按值存储,则存储起来的w值不会改变。
答案揭晓,w是按值存储的。可是,了解答案的唯一途径,就是牢记std::bind的工作原理。在std::bind调用中,答案是无迹可寻的。对比之下,采用lambda式的途径,w无论是按值或按引用捕获,在代码中都是显明的:
auto compressRateL = //w是按值捕获[w](CompLevel lev) //lev是按值传递{ return compress(w, lev); };
同样显明的还有形参的传递方式。在这里,形参lev清清楚楚地是按值传递的。因此:
compressRateL(CompLevel::High); //实参是按值传递
但在std::bind返回的结果对象里,形参的传递方式又是什么呢?
compressRateB(CompLevel::High); //实参是采用什么方式传递的?
还是那句话,欲知答案,唯一的途径是牢记std::bind的工作原理(答案是绑定对象的所有实参都是按引用传递的,因为此种对象的函数调用运算符利用了完美转发)。
总而言之,比起lambda式,使用std::bind的代码可读性更差、表达力更低,运行效率也可能更糟。在C++14中,根本没有使用std::bind的适当用例。而在C++11中,std::bind仅在两个受限的场合还算有着使用的理由:
- 移动捕获。C++11的lambda式没有提供移动捕获特性,但可以通过结合std::bind和lambda式来模拟移动捕获。欲知详情。参见条款32,统一条款还解释了c++14提供了初始化捕获的语言特性,从而消除了如此进行模拟的必要性。
- 多态函数对象。因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何型别的实参(除了在条款30讲过的那些完美转发的限制情况)这个特点在你想要绑定的对象具有一个函数调用运算符模板时,是有利用价值的。例如,给定一个类:
class PolyWidget
{
public:template<typename T>void operator()(const T& param);...
};
std::bind可以采用如下方式绑定polyWidget型别的对象:
PolyWidget pw;
auto boundPW = std::bind(pw, _1);
这么依赖,boundPw就可以通过任意型别的实参加以调用:
boundPW(1930); //传递int给PolyWidget::operator()
boundPW(nullptr); //传递nullptr给PolyWidget::operator()
boundPW("Rosebud"); //传递字符串字面量给PolyWidget::operator()
在C++11中的lambda式,是无法达成上面的效果的。但是在C++14中,使用带有auto型别形参的lambda式就可以轻而易举地达成同样的效果:
auto boundPW = [pw](const auto& param) //C++14{ pw(param); };
这些都是边缘用例,而即使这些边缘用例也会转瞬即逝,因为支持C++14的lambda式的编译器已经日渐普及。
当2005年bind被非正式地加入C++时,比其它在1998年的前身已经有了长足的进度,而C++11中加入的lambda式则是的std::bind相形见绌。到了C++14的阶段,std::bind已经彻底失去了用武之地。
要点速记:
- lambda式比起使用std::bind而言,可读性更好、表达力更强,可能运行效率也更高。
- 仅在C++11中,std::bind在实现移动捕获,或是绑定到具备模板化的函数调用运算符的对象的场合中,可能尚有余热可以发挥。
注:
std::bind总是复制其实参,但调用方却可以通过对某实参实施std::ref的手法达成按引用存储之的效果,下述语句:
auto compressRateB = std::bind(compress, std::ref(W), _1);
结果就是compressRateB的行为如同持有的是个指涉到w的引用,而非其副本。