Effective Modern C++ 条款34 比起std::bind更偏向使用lambda
2016-09-10 15:42
691 查看
比起std::bind更偏向使用lambda
C++11的std::bind是C++98的std::bind1st和std::bind2nd的继承人,但是,通俗的说,std::bind在2005年的时候已经是标准库的一部分了,那个时候标准委员会采用了名为TR1的文档,里面就包含std::bind的说明。(在TR1中,bind在不同的命名空间,所以它是std::tr1::bind,而不是std::bind,接口和现在有点不同。)这个历史意味着一些开发者对std::bind已经有了十年的或者更多的开发经验了,如果你是他们中的一员,你可能不愿意放弃这个工作得好好的工具。这是可以理解的,但是在如今的情况下,作出改变是好的,因为在C++11,比起使用std::bind,lambda几乎总是更好的选择。到了C++14,lambda在这种情况中不只是变强了,它还披上了装甲。该条款假设你熟悉std::bind,如果你不熟悉,那么在继续看下去之前,你要对它有基本的认识。这种的认识在某些情况下是值得的,因为你不会知道,在哪一个时间点,看代码或者维护代码时遇到std::bind。
就像条款32所说,我std::bind返回的函数对象称为绑定对象(bind object)。
比起std::bind更偏爱lambda的最主要原因是lambda的具有更好的可读性。举个例子,假设我们有个函数用来设置警报:
// 声明一个时间点的类型别名 using Time = std::chrono::steady_clock::time_point; // 关于"enum class"看条款10 enum class Sound {Beep, Siren, Whistle }; // 声明一个时间长度的类型别名 using Duration = std::chrono::steady_cloak::duration; // 在时间点t,发出警报声s,持续时间为d void setAlarm(Time t, Sound s, Duration d);
进一步假设,在程序的某些地方,我们想要设置在一个小时之后发出警报,持续30秒。但是呢,警报的类型,依然是未决定的。我们可以写一个修改了
setAlarm接口的lambda,从而只需要指定警报类型:
// setSoundL("L"指lambda)是一个允许指定警报类型的函数对象 // 警报在一个小时后触发,持续30秒 auto setSoundL = [](Sound s) { using namespace std::chrono; setAlarm(steady_clock::now() + hours(1), s, seconds(30)); };
注意看lambda里的
setAlarm,这是一个正常的函数调用,就算只有一点lambda经验的读者都可以看出传递给lambda的参数会作为
setAlarm的一个实参。
我们可以使用C++14对于秒(s),毫秒(ms),时(h)等标准后缀来简化代码,那是基于C++11的支持而照字面意思定义的。这些后缀在std::literals命名空间里实现,所以上面的代码可以写成这样:
auto setSoundL = [](Sound s) { using namespace std::chrono; using namespace std::literals; // 为了得到C++14的后缀 setAlram(steady_clock::now() + 1h, // C++14, 不过意思和上面相同 s, 30s); };
我们第一次尝试写出对应的std::bind调用,代码在下面。我们在注释中说明它有个错误,但是正确的代码复杂得多,而这个简化的版本可以让我们看到重要的问题:
using namespace std::chrono; using namespace std::literals; using namespace std::placeholders; // "_1"需要这个命名空间 auto setSoundB = // "B"对应"bind" std::bind(setAlarm, steady_clock::now() + 1h, // 错误!看下面 _1, 30s);
这份代码的读者简单地知道在
setSoundB里,std::bind会用指定时间点和持续时间来调用
setAlarm。对于缺少经验的读者,占位符“_1”简直是个魔术,为了理解
setSoundB的第一个实参会传递给
setAlarm的第二个参数,读者需要聪明地把std::bind参数列表上占位符的数字和它的位置进行映射。这个实参的类型在std::bind没有说明,所以读者还需要去咨询
setAlarm的声明,来决定传递给
setSoundB的参数类型。
但是,如我所说,这代码不完全正确。在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:
auto setSoundB = std::bind(setAlarm, std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s);
如果你熟悉来自C++98的std::plus,你可能会对这份代码感到惊奇,因为在两个方括号之间没有指定类型,即代码含有
std::plus<>,而不是
std::plus<type>。在C++14,标准操作符模板的模板类型参数可以被省略,所以这里提供类型给它。C++11没有提供这种特性,所以在C++11中对于lambda的std::bind等同物是这样的:
using namespace std::chrono; using namespace std::placeholders; auto setSoundB = set::bind(setAlarm, std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)), _1, seconds(30));
如果,在现在这个时刻,lambda的实现看起来没有吸引力的话,你可能需要去检查一下视力了。
当
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; using namespace std::literals; setAlarm(steady_clock::now + 1h, // 正确,调用 s, // 3参数版本的 30s); // setAlarm };
另一方面,std::bind的调用,现在会编译失败:
auto setSoundB = std::bind(setAlarm, std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s);
问题在于编译器没有办法决定哪个
setAlarm应该被传递给std::bind,它拥有的只是一个函数名,而这单独的函数名是有歧义的。
为了让std::bind可以通过编译,
setAlarm必须转换为合适的函数指针类型:
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d); auto setSoundB = // 现在就ok了 std::bind(static_cast<SetAlarm3ParamType>(setAlarm), std::bind(std::plus<>(), steady_clocl::now() 1h), _1, 30s);
但这又引出了lambda和std::bind的另一个不同之处。在
setSoundL的函数调用操作符内(即,lambda的闭包类的函数调用操作符),是以普通函数调用的方式调用
setAlarm,这可以被编译器以通用的方式内联:
setSoundL(Sound::Siren); // setAlarm的函数体可能在这里内联
不过,在std::bind的调用中,传递了一个指向
setAlarm的函数指针,而那意味着在
setSoundB的函数调用操作符内(即,绑定对象的函数调用操作符),是以函数指针的方式调用
setAlarm,而那意味着通过
setSoundB调用的
setAlarm,比通过
setSoundL调用的
setAlarm进行内联的可能性更低:
setSoundB(Sound::Siren); // setAlarm的函数体在这里内联的可能性较低
因此,使用lambda生成的代码可能会比使用std::bind的快。
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,你还必须指定要比较的类型,所以std::bind的调用看起来是这样的:
auto betweenB = // c++11版本 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形参,所以它也必须指定类型:
auto betweenL = C++11版本 [lowVal, highVal] (int val) { return lowVal <= val && val <= highVal; };
不管怎样,我希望我们能认同lambda的版本不仅代码更短,还具有更好的可读性和可维护性。
在早些时候,我提起过对于那些对std::bind没有经验的程序员,占位符(例如,_1,_2等)跟是魔术一样。不过,占位符的行为不是完全密封的。假设我们有一个用来精简拷贝Widget的函数,
enum class CompLevel { low, Normal, High }; // 精简等级 Widget compress(const Widget& w, CompLevel lev); // 对w进行精简拷贝
然后我们想要创建一个函数对象,它允许我们指定
Widget w的精简级别,这是用std::bind创建的函数对象:
Widget w; using namespace std::placeholders; auto compressRateB = std::bind(compress, w, _1);
当我们把
w传递给std::bind时,为了以后的
compress调用,
w会被存储起来,它存储在对象
compressRateB中,但它是如何存储的呢——通过值还是引用呢?这是会导致不一样的结果,因为如果
w在调用std::bind和调用
compressRateB之间被修改,通过引用存储的
w也会随之改变,而通过值存储就不会改变。
答案是通过值存储,你想知道答案的唯一办法就是知道std::bind是如何工作的;但在std::bind中没有任何迹象。对比使用lambda方法,
w通过值捕获或通过引用捕获都是显式的:
auto compressRateL = [w](CompLevel lev) // w以值捕获,lev以值传递 { return compress(w, lev); };
参数以何种方式传递也是显示的。在这里,很清楚地知道参数
lev是以值传递的。因此:
CompressRateL(CompLevel::High); // 参数以值传递
但在绑定对象里,参数是以什么方式传递的呢?
compressRateB(ConpLevel::High); // 参数传递方式?
再次说明,想知答案的唯一办法是记住std::bind是怎样工作的。(答案是传递给绑定对象的所有参数都是通过引用的方式,因为绑定对象的函数调用操作符使用了完美转发。)
那么,对比lambda,使用std::bind的代码可读性不足、表达能力不足,还可能效率低。在C++14,没有理由使用std::bind。而在C++11,std::bind可以使用在受限的两个场合:
移动捕获。C++11的lambda没有提供移动捕获,但可以结合std::bind和lambda来效仿移动捕获。具体细节看条款32,那里也解释了C++11效仿C++14的lambda提供的初始化捕获的情况。
多态函数对象。因为绑定对象的函数调用操作符会使用完美转发,它可以接受任何类型的实参(条款30讲述了完美转发的限制)。这在你想要绑定一个函数调用操作符模板时有用。例如,给定这个类:
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); // 传递int到PolyWidget::operator() boundPW(nullptr); // 传递nullptr到PolyWidget::operator() boundPW("Rosebud"); // 传递字符串到PolyWidget::operator()
这在C++11的lambda里无法做到,但是在C++14,使用auto形参就很容易做到了:
auto boundPW = [pw](const auto& param) { pw(param); }
当然,这些都是边缘情况,而且这种边缘情况会转瞬即逝,因为支持C++14的编译器已经越来越普遍。
2005年,bind非官方地加入了C++,比起它的前身有了很多的进步。而在C++11,lambda几乎要淘汰std::bind,而在C++14,std::bind已经没有需要使用的场合了。
总结
需要记住的2点:比起使用std::bind,lambda有更好的可读性,更强的表达能力,可能还有更高的效率。
在C++11,只有在实现移动捕获或者绑定函数调用操作符模板时,std::bind可能是有用的。
相关文章推荐
- Effective Modern C++ 条款21 比起直接使用new,更偏爱使用std::make_unique和std::make_shared
- Effective Modern C++ 条款32 对于lambda,使用初始化捕获来把对象移动到闭包
- Effective Modern C++ 条款20 把std::weak_ptr当作类似std::shared_ptr的、可空悬的指针使用
- Effective Modern C++ 条款31 对于lambda表达式,避免使用默认捕获模式
- Effective Modern C++ 条款33 对需要std::forward的auto&&参数使用decltype
- Effective Modern C++ 条款25 对右值引用使用std::move,对通用引用使用std::forward
- Effective Modern C++ 条款22 当使用Pimpl Idiom时,在实现文件中定义特殊成员函数
- Effective Modern C++ 条款19 用std::shared_ptr管理共享所有权的资源
- Effective modern C++ 条款 38:如果异步至关重要请指定std::launch::async
- Effective Modern C++ 条款6 当auto会推断出不合理的类型时使用显式类型初始化语法
- Effective Modern C++ 条款37 在所有路径上,让std::thread对象变得不可连接(unjoinable)
- Effective Modern C++ 条款36 如果异步执行是必需的,指定std::launch::async策略
- Effective modern C++ 条款 39:让std::thread在所有路径上不可join(Make std::threads unjoinable on all paths)
- Effective Modern C++ 条款18 用std::unique_ptr管理独占所有权的资源
- Effective Modern C++ 条款15 尽可能使用constexpr
- Effective Modern C++ 34. Prefer lambdas to std::bind
- Effective Modern C++ 条款23 理解std::move和std::forward
- C++中std::tr1::function和bind 组件的使用
- Effective C++——》条款6:若不想使用编译器自动生成函数,就该明确拒绝 .
- Effective Modern C++翻译(2)-条款1:明白模板类型推导