概述
(原文链接: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.
最后
以上就是追寻手机为你收集整理的Google C++每周贴士 #108: 避免使用std::bind每周贴士 #108: 避免使用std::bind的全部内容,希望文章能够帮你解决Google C++每周贴士 #108: 避免使用std::bind每周贴士 #108: 避免使用std::bind所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复