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在实现移动过捕获,或是绑定到具备模板化的函数调用运算符的对象的场合中,才能发挥发挥余热;