(原文链接:https://abseil.io/tips/108 译者:clangpp@gmail.com)
每周贴士 #108: 避免使用std::bind
- 最初发布于2016-01-07
- 作者:Roman Perepelitsa (roman.perepelitsa@gmail.com)
- (译者注:这哥们儿是C++大神,有兴趣可以上网搜搜他的文章和代码)
- (译者注:现在离开Google去做举重运动员了,大写的佩服!)
- 更新于2019-12-19
- 短链接:abseil.io/tips/108
避免使用std::bind
这条贴士总结了为什么你写代码应该远离std::bind()的原因。
正确使用std::bind()太难了。一起来看几个例子。这段代码看起来行不?
void DoStuffAsync(std::function<void(Status)> cb);
class MyClass {
void Start() {
DoStuffAsync(std::bind(&MyClass::OnDone, this));
}
void OnDone(Status status);
};
很多C++老油条工程师们写过类似的代码,然后发现编译不过。std::function<void()>(译者注:函数签名里没有参数)用起来好好的,但是给MyClass::OnDone加个参数就跪了。什么情况?
std::bind()不只是绑定靠前的N个参数,这与很多C++工程师预期的行为(偏函数)(译者注:维基百科打不开的话,我没找到C++版本的中文解释,这个JavaScript的也可以凑合看,知道意思就行)不一致。你必须指定所有参数,所以催动std::bind()正确的咒语是:
std::bind(&MyClass::OnDone, this, std::placeholders::_1)
那个啥,真丑。有木有好点儿的方式?还真有,用absl::bind_front()。
absl::bind_front(&MyClass::OnDone, this)
还记得早前提到std::bind()没实现的偏函数吗?absl::bind_front()精准实现了这个功能:它绑定靠前的N个参数,然后完美转发剩下的参数:absl::bind_front(F, a, b)(x, y)展开成F(a, b, x, y)。
你看,世界又科学了。想来点儿刺激的不?下面的代码是什么行为?
void DoStuffAsync(std::function<void(Status)> cb);
class MyClass {
void Start() {
DoStuffAsync(std::bind(&MyClass::OnDone, this));
}
void OnDone(); // 没有Status参数.
};
OnDone()不接受参数,传给DoStuffAsync()的回调函数应该接受一个Status参数。你也许会预期编译错误,但实际上编译会成功,而且连条警告都没有,因为std::bind过于激进地弥合了两者的不一致(译者注:收不收Status参数)。DoStuffAsync()里可能出现的错误(译者注:表现为Status对象,在传给回调函数时)被悄悄地忽略了。
这样的代码有可能带来严重伤害。比如一个输入输出操作跪了,但是调用端以为它成功了,那酸爽可能是毁灭性的。也许MyClass的作者根本没意识到DoStuffAsync()有可能出现一个本应被处理的错误。或者DoStuffAsync()以前接收std::function<void()>参数,但后来作者决定引入错误状态,然后手动更新所有编译报错的调用端代码。不管是哪种情况,bug就这样溜进了生产环境的代码。
std::bind()瘫痪了我们强烈依赖的编译期检查。如果调用端给你的函数传了多余的参数,通常编译器会告诉你一声,但std::bind()让编译器哑火了。你以为这就够刺激了?
再来个例子。你认为这段代码怎么样?
void Process(std::unique_ptr<Request> req);
void ProcessAsync(std::unique_ptr<Request> req) {
thread::DefaultQueue()->Add(
ToCallback(std::bind(&MyClass::Process, this, std::move(req))));
}
跨作用域传递std::unique_ptr的经典方式。甭问,std::bind()肯定不灵——这段代码编译不过,因为std::bind()不支持把只能移动(move-only)(译者注:不能复制)的参数传递给目标函数。把std::bind()替换为absl::bind_front()就行了。
下一个例子,就算是C++专家,也通常会绊一跟头。看看你能不能发现其中的问题。
// F必须是接收0个参数的可调用对象。
template <class F>
void DoStuffAsync(F cb) {
auto DoStuffAndNotify = [](F cb) {
DoStuff();
cb();
};
thread::DefaultQueue()->Schedule(std::bind(DoStuffAndNotify, cb));
}
class MyClass {
void Start() {
DoStuffAsync(std::bind(&yClass::OnDone, this));
}
void OnDone();
};
这段代码编译不过,因为把std::bind()的结果传递给另一个std::bind()是个特殊情况。通常情况下,std::bind(F, arg)()展开成F(arg)。但如果arg是另一个std::bind()的结果时,它展开成F(arg())。如果先把arg转化为std::function<void()>,这个神奇的特殊行为就没了。
将std::bind()用于不归你控制的类型是一个bug。DoStuffAsync()不该将std::bind()用于模板参数上。改用absl::bind_front()或lambda就行了。
DoStuffAsync()的作者甚至有可能看到测试一路绿灯,因为单元测试里永远会丢给它lambda或std::function做参数,但永远不会把std::bind()的结果丢给它。MyClass的作者撞上这个bug的时候肯定一脸懵。
退一万步讲,std::bind()的特殊行为有用吗?毛用没有。它就是块绊脚石。如果你正试图通过嵌套调用std::bind()来组合函数,你真的应该写个lambda或者普通函数。
希望你已经接受了“std::bind()容易被用错”这个观点。运行期和编译器的陷阱既坑新手又坑C++专家。现在我想展示给你:就算std::bind()被用对了,通常也会有可读性更高的替代方案。
不用占位符(placeholder)的std::bind()不如改用lambda。
std::bind(&MyClass::OnDone, this)
对比
[this]() { OnDone(); }
用std::bind()实现的偏函数不如改用absl::bind_front()。占位符越多,差距越明显。
std::bind(&MyClass::OnDone, this, std::placeholders::_1)
对比
absl::bind_front(&MyClass::OnDone, this)
(实现偏函数的时候,用absl::bind_front()还是用lambda可以看着办,自己决定。)
这里覆盖了99%的std::bind()调用场景。剩下的场景就比较秀了:
- 忽略掉部分参数:
std::bind(F, _2)。 - 同一个参数用多次:
std::bind(F, _1, _1)。 - 绑定靠后的参数:
std::bind(F, _1, 42)。 - 改变参数顺序:
std::bind(F, _2, _1)。 - 组合函数:
std::bind(F, std::bind(G))。 - 以上任意一种,外加结果要求为多态函数对象。
这些进阶应用也许有其用武之地。在决定使用它们之前,先想想已知的std::bind()的坑,然后问问你自己,省下的几个字符或几行代码是不是值得付出这么大代价。
结论
避开std::bind。改用lambda或absl::bind_front。
延伸阅读
'’Effective Modern C++’’, Item 34: Prefer lambdas to std::bind.

本文探讨了std::bind在C++编程中常见的问题与陷阱,包括编译期检查失效、参数处理不当及与std::unique_ptr配合使用时的难题。介绍了absl::bind_front作为更佳替代方案的优势,并建议使用lambda表达式提升代码可读性。
188

被折叠的 条评论
为什么被折叠?



