条款34:首选 Lambda 而不是 std::bind
1 Lambda 与 std::bind
- 在C++ 11中,lambdas几乎总是比std::bind更好的选择,从C++ 14开始,lambda只是更强了,完全是无懈可击。
- 比起 std::bind,更倾向于使用 lambda 的最重要原因是它更具可读性。例如,假设有一个设置警报的函数:
using Time = std::chrono::steady_clock::time_point;
enum class Sound { Beep, Siren, Whistle };
using Duration = std::chrono::steady_clock::duration;
// 在时间 t,发出声音 s,持续时间 d
void setAlarm(Time t, Sound s, Duration d);
// L” 代表 “lambda”,是一个函数对象,一小时后响起30秒警报,允许指定声音
auto setSoundL =[](Sound s)
{
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1), // 一小时后响起的警报
s, // 设置声音
seconds(30)); // 持续30秒
};
在 C++14 中,可以利用 标准后缀来简化这段代码。这些后缀在 std::literals 命名空间中实现:
auto setSoundL = [](Sound s)
{
using namespace std::chrono;
using namespace std::literals;
setAlarm(steady_clock::now() + 1h,
s,
30s);
};
首次尝试编写相应的 std::bind 调用如下:
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders; // 用于 "_1"
//调用 setSoundB 会使用在调用 std::bind 中指定的时间和持续时间来调用 setAlarm
//对 setSoundB 的调用中的第一个参数作为第二个参数传递给 setAlarm
auto setSoundB = std::bind(setAlarm, // “B” 代表 "bind"
steady_clock::now() + 1h, // 错误!
_1,
30s);
//在 std::bind 调用中,“steady_clock::now() + 1h”作为参数传递给 std::bind,而不是 setAlarm。该表达式将在调用 std::bind 时进行求值,并将该表达式产生的时间存储在生成的bind对象中
解决这个问题需要告诉 std::bind 推迟表达式的求值,直到调用 setAlarm,而做到这一点的方法是将第二个对 std::bind 的调用嵌套在第一个调用中:
auto setSoundB =std::bind(setAlarm,
std::bind(std::plus<>(), steady_clock::now(), 1h),
_1,
30s);
代码包含“std::plus<>”,而不是“std::plus”。在 C++14 中,标准操作符模板的模板类型参数通常可以省略。C++11 没有提供这样的功能,因此与 lambda 等效的 C++11 std::bind 是:
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;
setAlarm(steady_clock::now() + 1h,
s,
30s);
};
另一方面,std::bind调用现在无法编译:
auto setSoundB = std::bind(setAlarm, // 错误!哪个setAlarm?
std::bind(std::plus<>(),
steady_clock::now(),
1h),
_1,
30s);
要使 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);
在 setSoundL 的函数调用运算符内部(即 lambda 闭包类的函数调用运算符),对 setAlarm 的调用是一种普通的函数调用,编译器可以按照通常的方式对其进行内联:
setSoundL(Sound::Siren); // setAlarm 的主体可能在这里内联
std::bind 对 setAlarm 的调用是通过函数指针进行的。编译器不太可能内联通过函数指针的函数调用:
setSoundB(Sound::Siren); // setAlarm 的主体不太可能在这里内联
- lambda 也适合更复杂的业务逻辑。例如,考虑这个 C++14 lambda ,它返回其参数是否在最小值(lowVal)和最大值(highVal)之间,其中 lowVal 和 highVal 是局部变量:
auto betweenL =[lowVal, highVal](const auto& val) // C++14
{ return lowVal <= val && val <= highVal; };
std::bind可以表达同样的意图,但是代码晦涩难懂:
using namespace std::placeholders;
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 = std::bind(std::logical_and<bool>(),// C++11 version
std::bind(std::less_equal<int>(), lowVal, _1),
std::bind(std::less_equal<int>(), _1, highVal));
当然,在c++ 11中,lambda不能接受auto形参,所以它也必须提交一个类型:
auto betweenL = [lowVal, highVal] // C++11 version
(int val)
{ return lowVal <= val && val <= highVal; };
对于那些没有多少 std::bind 使用经验的人来说,它的占位符(例如,_1、_2 等)本质上是神奇的。但不透明的不仅仅是占位符的行为。假设有一个函数来创建 Widget 的压缩副本:
enum class CompLevel { Low, Normal, High }; // 压缩级别
Widget compress(const Widget& w, CompLevel lev); // 制作w的压缩副本
并且想要创建一个函数对象,能够指定压缩级别。对 std::bind 的使用将创建这样一个对象:
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);
当将 w 传递给 std::bind 时,它必须被存储以供稍后调用 compress 时使用。它存储在对象 compressRateB 中,但是它是按值还是按引用存储的呢?答案是按值存储的。
//使用 std::bind 时,无法直接看出 w 是按值还是按引用存储的,而使用 lambda 则可以明确地指定 w 的捕获方式。这样可以更好地理解和控制代码的行为。
auto compressRateL = // w 是值捕获
[w](CompLevel lev)
{ return compress(w, lev); }; // lev 是值传递
同样明确的是如何将参数传递给 lambda。在这里,很明显参数 lev 是按值传递的。因此:
compressRateL(CompLevel::High); // 参数按值传递
但是在对由 std::bind 产生的对象的调用中,参数是如何传递的呢?同样,知道的唯一方法是记住 std::bind 的工作方式。(答案是传递给绑定对象的所有参数都是按引用传递的,因为此类对象的函数调用运算符使用了完美转发。)
compressRateB(CompLevel::High); // 参数是如何传递的?
- 与 lambda 表达式相比,使用 std::bind 的代码可读性较差、表达力较弱,并且可能效率较低。在 C++14 中,没有合理的使用 std::bind 的情况。然而,在 C++11 中,std::bind 在两种情况下可以证明是合理的:
1)移动捕获:C++11 的 lambda 表达式不提供移动捕获,但可以通过 lambda 和 std::bind 的组合来模拟。
2)多态函数对象(Polymorphic function object):调用运算符可以接受任何类型参数的可调用对象,这在绑定具有模板化函数调用运算符的对象时很有用。例如,给定这个类:
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);
boundPW(nullptr);
boundPW("Rosebud");
在C++ 11 lambda中没有办法做到这一点。然而,在C++ 14中,它很容易通过带有auto参数的lambda实现:
auto boundPW = [pw](const auto& param) { pw(param); }; // C++14
2 要点速记
- Lambda 比使用 std::bind 更具可读性、表达力,并且可能更高效。
- 仅在 C++11 中,std::bind 可能对于实现移动捕获或绑定具有模板化函数调用运算符的对象有用。
更多推荐


所有评论(0)