优先选择lambda式,而非std::bind
std::bind
是C++98中std::bind1st
和std::bind2nd
的后继特性,但是,作为一种非标准特性而言,std::bind
在2005年就已经是标准库的组成部分了。正是在那是,标准委员会接受了名称TR1的文档,里面就包含了std::bind
的规格(在TR1中,bind
位于不同的名字空间,所以是std::tr1::bind
而非std::bind
)。
在C++11中,相对于std::bind
,lambda
几乎总会是更好的选择。到了C++14,lambda
不仅是优势变强,简直已称为不二之选。
之所以说优先选择lambda
,最主要原因是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
的接口。这个新的接口只需指定声音即可:
//它接受一个声音
//该声音将在设定后1小时发出,并持续30秒
auto setSoundL = [](Sound s)
{
using namespace std::chrono;
setAlaram(steady_clock::now() + hours(1), s, seconds(30));
};
我们可以利用C++14所提供的秒(s),毫秒(ms)和小时(h)等标准后缀来简化上述代码,这一点建立在C++11中用户定义字面量这项能力的支持之上。这些后缀在std::literals
名字空间里实现,所以上面的代码可以重写如下:
auto setSoundL = [](Sound s)
{
using namespace std::chrono;
using namespace std::literals;
setAlaram(steady_clock::now() + 1h, s, 30s);
};
下面的代码就是我们撰写对应的std::bind
调用语句的首次尝试,这段代码包含一个错误,我们一会来修复它,修正后的代码会复杂很多。但是,即使是这个简化的版本可以让我们看到一些重要议题。
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
auto setSoundB = std::bind(setAlarm,
steady_clock::now() + 1h, _1, 30s);
在读这段代码时,只需了解,在调用setSoundB
时,会使用在调用std::bind
时指定的时刻和时长来唤起setAlarm
。对于占位符_1
,要理解在调用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++14中,标准运算符模板的模板类型实参大多数情况下可以省略不写,所以此处也就没有必要提供了。而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));
一旦对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;
setAlaram(steady_clock::now() + hours(1), s, seconds(30));
};
不过,对std::bind
的调用,现在可就无法通过编译了。
auto setSoundB = std::bind(setAlarm,
std::bind(std::plus<>(), steady_clock::now(), 1h),
_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
要低。
综上所述,使用lambda
式就有可能生成比使用std::bind
运行得更快的代码。
总而言之,比起lambda
式,使用std::bind
的代码可读性更差,表达力更低,运行效率也可能更糟。在C++14中,根本没有使用std::bind
的适当用例。而在C++11中,std::bind
仅在两个受限的场合还算有着使用的理由。
- 移动捕获。C++11的
lambda
式没有提供移动捕获特性,但可以通过结合std::bind
和lambda
来模拟移动捕获。 - 多态函数对象。因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何类型的实参。这个特点在你想要绑定的对象具有一个函数调用运算符模板时,是有利用价值的。
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
式,是无法实现上面的效果的。
要点速记
lambda
式比起使用std::bind
而言,可读性更好,表达力更强,可能运行效率也更好- 仅在C++11中,
std::bind
在实现移动捕获,或是绑定到具备模板化的函数调用运算符的对象的场合中,可能尚有余温可以发挥。