《Effective Modern C++》学习笔记之条款三十四:优先选用lambda表达式,而非std::bind

优势1:可读性更好

我们假设有个函数用来设置声音警报,示例代码:

// 表示时刻的型别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的优势了,可读性强了不少。

优势2:遇到重载也没事

一旦对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));  

优势3:可能性更高的生成更快的代码

但即便你觉得这样写好,多写几行没什么问题,但这么写还是带来了更大的问题:
在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));


这么一对比,已经太明显不过了。

优势4:参数类型一目了然

试想这样一个场景,需要压缩一个类,然后返回这个类的副本:

enum class CompLevel { Low, Normal, High};

Widget compress(const Widget& w, CompLeve leve)

然后写了个函数对象包装一下:

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);        //这里是按照什么呢?

答案又会让你出乎意料,而且还是死记硬背没有原因的:

结论

C++14里已经可以忘记std::bind了。

C++11里还有两种受限场合可以使用:

  1. 移动捕获:C++11语言不支持初始化捕获,只能用bind来模拟
  2. 多态函数对象: 因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何型别的实参。这个特点再你想要绑定的对象具有一个函数调用运算符模板是,是有利用价值的。

例如:

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);};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiang木

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值