Item 34: Prefer lambdas to std::bind

C++ 11的std::bind 是对C++ 98中std:bind1st 和 std::bind2nd 的继承,它在 2005 年以 TR1 文档形式非正式地成为标准库的一部分。因此,许多 C++ 程序员可能已经有十几年的 std::bind 使用经验,现在想劝退他们不使用std::bind,有点不容易。但是,做出改变总是好的。在c++ 11中,lambdas几乎总是比std::bind更好的选择。而从c++ 14开始,lambdas变得更强大。

lambda的第一个优势,也是最主要的优势,就是可读性更好。假设有一个设置声音报警的函数:

// typedef for a point in time (see Item 9 for syntax)
using Time = std::chrono::steady_clock::time_point;

// see Item 10 for "enum class"
enum class Sound { Beep, Siren, Whistle };

// typedef for a length of time
using Duration = std::chrono::steady_clock::duration;

// at time t, make sound s for duration d
void setAlarm(Time t, Sound s, Duration d);

假设需求变为在设置闹钟后的一个小时后响起,并持续30秒,报警声音没有具体设定。我们可以编写一个lambda来修改setAlarm的接口:

// setSoundL ("L" for "lambda") is a function object allowing a
// sound to be specified for a 30-sec alarm to go off an hour
// after it's set
auto setSoundL =
    [](Sound s)
    {
        // make std::chrono components available w/o qualification
        using namespace std::chrono;
        
        setAlarm(steady_clock::now() + hours(1),    // alarm to go off
                s,                                  // in an hour for
                seconds(30));                       // 30 seconds
    };

如果使用 C++14 字面值 std::literals 改写上面代码,可以更加简洁:

auto setSoundL =
[](Sound s)
{
    using namespace std::chrono;
    using namespace std::literals;          // for C++14 suffixes
    
    setAlarm(steady_clock::now() + 1h,      // C++14, but
             s,                             // same meaning
             30s);                          // as above
};

如果使用 std::bind 直接替换 lambda 表达式,可以改写成如下:

using namespace std::chrono;                // as above
using namespace std::literals;

using namespace std::placeholders;          // needed for use of "_1"

auto setSoundB =                            // "B" for "bind"
    std::bind(setAlarm,
              steady_clock::now() + 1h,     // incorrect! see below
              _1        ,
30s);

首先,这段代码在可读性上就比lambda差太多了,还有一个 “_1”。调用setSoundB的时候还得查看一下setAlarm的声明,才能知道这里的占位符的传参类型。
另外,这段代码的逻辑是有BUG的。在std::bind的调用中,“steady_clock::now() + 1h”是作为实参传给std::bind,而非setAlarm。也就是说表达式评估求值是在std::bind被调用时,然后保存在绑定对象内部。这就导致在调用绑定对象时,再传回给setAlarm的时间不是我们想要的调用setAlarm的时刻了。

为了解决这个问题,可以通过在原来的std::bind里再嵌套一个std::bind,用来告诉外层的std::bind延迟求值表达式的评估值,直到调用setAlarm:

auto setSoundB =
  std::bind(setAlarm,
            std::bind(std::plus<>(), steady_clock::now(), 1h),  // C++14
            _1,
            30s);

注意到 std::plus<> 缺省了类型参数,这是 C++ 14 的新特性,如果是 C++11,则需要指定类型:

auto setSoundB =
  std::bind(setAlarm,
            std::bind(std::plus<steady_clock::time_point>(), // C++11
                      steady_clock::now(),
                      hours(1)),
            _1,
            seconds(30));

这么看来,std::bind真是不咋地。

如果再来一个setAlarm的重载,新的问题就来了。增加一个指定音量的形参:

enum class Volume { Normal, Loud, LoudPlusPlus };

void setAlarm(Time t, Sound s, Duration d, Volume v);

之前那个 lambda 版本代码依然可以正常工作。但是,std::bind 将会产生编译报错。因为编译器无法确认传递给 std::bind 的是哪个哪个版本的 setAlarm。它拿到的所有信息只有一个函数名字,而这个函数名还是多义的。

auto setSoundB =                                // error! which
    std::bind(setAlarm,                         // setAlarm?
              std::bind(std::plus<>(),
                        steady_clock::now(),
                        1h),
              _1,
              30s);

为了能通过编译,只能将 setAlarm 强转为正确的函数指针类型:

using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);

auto setSoundB =                                        // now
  std::bind(static_cast<SetAlarm3ParamType>(setAlarm),  // okay
            std::bind(std::plus<>(),
                      steady_clock::now(),
                      1h),
            _1,
            30s);

但这又带了另一个lambda 和 std::bind的区别。在setSoundL的函数调用运算符中(即,lambda式所对应的闭包类的函数调用运算符中)调用setAlarm采用的是常规的函数唤起方式,这样的话,编译器就可以使用内联:

setSoundL(Sound::Siren); // body of setAlarm may
                         // well be inlined here

然而,在std::bind的调用中,传递的是一个指向setAlarm的函数指针,这就意味着在setSoundB的调用时,setAlarm的调用是通过函数指针调用的。这属于运行期的行为,编译器不太会内联。

setSoundB(Sound::Siren); // body of setAlarm is less
                         // likely to be inlined here

因此lambda的第二个优势,就是执行性能可能会高于std::bind。

以上仅仅是一个简单函数调用的例子。如果想做的事情更复杂一些,那么lambda的优势将更加明显。考虑如下代码,判断一个值是否在某区间范围内,lowVal and highVal都是局部变量:

auto betweenL =
    [lowVal, highVal]
    (const auto& val)                              // C++14
    { return lowVal <= val && val <= highVal; };

std::bind也可以做同样的事情,不过要想正常的工作,需要用很晦涩的方式来构建代码:

using namespace std::placeholders;                  // as above
    auto betweenB =
        std::bind(std::logical_and<>(),             // C++14
                    std::bind(std::less_equal<>(), lowVal, _1),
                    std::bind(std::less_equal<>(), _1, highVal));

如果用C++ 11实现,还要指定类型:

auto betweenB =                                     // C++11 version
    std::bind(std::logical_and<bool>(),
                std::bind(std::less_equal<int>(), lowVal, _1),
                std::bind(std::less_equal<int>(), _1, highVal));

当然,如果在C++ 11中,lambda式中不能使用auto类型的参数,所以lambda版代码如下:

auto betweenL =                                     // C++11 version
    [lowVal, highVal]
    (int val)
    { return lowVal <= val && val <= highVal; };

说了这么多,其实就是想说,lambda表达式比std::bind的代码更小,更易理解和维护。


假设有一个函数,用来创建Widget的压缩副本:

enum class CompLevel { Low, Normal, High };     // compression level
    
Widget compress(const Widget& w,                // make compressed
                CompLevel lev);                 // copy of w

我们想创建一个函数对象,用来指定特定 Widget 的压缩等级。使用 std::bind 创建函数对象:

Widget w;

using namespace std::placeholders;

auto compressRateB = std::bind(compress, w, _1);

当我们将 w 传给std::bind时,它被存储起来了,供以后调用 compress 时使用。它是被存在compressRateB内的。但它是怎么被存储的呢——按值还是按引用?答案是:w按值存储。 我们只能硬记住这个答案。

【std::bind总是按值拷贝其实参,但是调用方可以利用std::ref实现按引用窜出的效果】:

auto compressRateB = std::bind(compress, std::ref(w), _1);

但如果用lambda表达式的话,w是按值捕获还是按引用捕获可以显示指出:

auto compressRateL =                // w is captured by
    [w](CompLevel lev)              // value; lev is
    { return compress(w, lev); };   // passed by value

在上面的代码中,形参的传递方式,也是清清楚楚的。因此:

compressRateL(CompLevel::High);     // arg is passed by value

但是,std::bind 的绑定对象的调用,参数是如何传递的?

compressRateB(CompLevel::High);     // how is arg passed?

同样地,唯一的办法是记住std::bind的工作原理 —— 传给绑定对象的所有实参都是通过引用传递的,因为绑定对象的函数调用运算符使用了完美转发。

总之,在C++ 14中,std::bind根本就没有合理的用例。也就是在c++ 11中,std::bind在两种受约束的情况下才有优势:

  • 移动捕获。C++ 14 的初始化捕获模式支持移动捕获。C++11 的 lambda 不支持移动捕获,可以使用 std::bind 模拟来间接实现,参见 Item32 ;
  • 多态函数对象。C++ 14 支持 auto 参数类型,也即通用 lambda,参见 Item33 。但是 C++11 不支持通用 lambda。而 std::bind 绑定对象的函数调用使用完美转发实现,可以接收任何类型的参数。如下例子:
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);          // pass int to
                        // PolyWidget::operator()

boundPW(nullptr);       // pass nullptr to
                        // PolyWidget::operator()

boundPW("Rosebud");     // pass string literal to
                        // PolyWidget::operator()

在C++ 11的lambda中,是没办法做到上面的事情的。但在C++ 14中,使用auto类型形参的lambda可以轻松做到同样的事情:

auto boundPW = [pw](const auto& param)  // C++14
                { pw(param); };

其实,到了C++ 14阶段,std::bind已经没什么用武之地了。

Things to Remember

  • Lambdas可读性更强,表达能力更强,可能比使用std::bind更高效;
  • 在c++ 11中,std::bind在实现移动过捕获,或是绑定到具备模板化的函数调用运算符的对象的场合中,才能发挥发挥余热;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值