条款34.优先选用lambda式而非bind

优先选择lambda式,而非std::bind

std::bind是C++98中std::bind1ststd::bind2nd的后继特性,但是,作为一种非标准特性而言,std::bind在2005年就已经是标准库的组成部分了。正是在那是,标准委员会接受了名称TR1的文档,里面就包含了std::bind的规格(在TR1中,bind位于不同的名字空间,所以是std::tr1::bind而非std::bind)。

在C++11中,相对于std::bindlambda几乎总会是更好的选择。到了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() + 1hsetAlarm的实参之一。该表达式会在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);

但这么做,lambdastd::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::bindlambda来模拟移动捕获。
  • 多态函数对象。因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何类型的实参。这个特点在你想要绑定的对象具有一个函数调用运算符模板时,是有利用价值的。
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在实现移动捕获,或是绑定到具备模板化的函数调用运算符的对象的场合中,可能尚有余温可以发挥。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值