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

2022/3/19 17:57:53

本文主要是介绍《Effective Modern C++》学习笔记之条款三十四:优先选用lambda表达式,而非std::bind,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 先来点历史资料

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

2. 再来谈抛弃

这样的历史意味着,有些开发者已经有了十多年的std::bind开发经验。如果你是他们中的一员,那你可能不太情愿放弃这么一个运作良好的工具。这可以理解。但是对于这个特定的情况,改变是有收益的。

因为在C++11中,相对于bind,lambda式几乎总会是更好的选择。到了C++14,lambda不仅优势变大,更成为了不二之选。

3. 前置条件

该条款假设你熟悉std::bind。如果你还不熟悉,那么在继续阅读前,需要先熟悉他们。

这里和Item 32一样,std::bind返回的函数对象叫做绑定对象。

4. 开始说正事儿,优势列举

4.1 优势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的优势了,可读性强了不少。

4.2 优势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));  

4.3 优势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.4 std::bind的劣势

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

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的工作原理,绑定对象的所有实参都是按引用传递的,因为此种对象的函数调用运算符利用了完美转发。

5. 结论

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);};

要点速记

  1.  lambda式比起使用std::bind而言,可读性更好,表达力更强,可能运行效率更高。

  2. 仅在C++11中,std::bind在实现移动捕获,或是多态函数对象的时候,还有余热可以发挥。
     



这篇关于《Effective Modern C++》学习笔记之条款三十四:优先选用lambda表达式,而非std::bind的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程