本周小贴士#108:避免std::bind
Posted -飞鹤-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本周小贴士#108:避免std::bind相关的知识,希望对你有一定的参考价值。
作为TotW#108最初发表于2016年1月7日
由Roman Perepelitsa创作
更新于2020年8月19日
避免std::bind
本贴士总结了在编写代码时为什么要远离std::bind的原因。
正确使用std::bind很难。让我们看几个例子。对你而言,这代码好看吗?
void DoStuffAsync(std::function<void(absl::Status)> cb);
class MyClass {
void Start() {
DoStuffAsync(std::bind(&MyClass::OnDone, this));
}
void OnDone(absl::Status status);
};
许多经验丰富的C++工程师编写了这样的代码,结果却发现它无法编译。它与std::function<void()>一起工作,但是当你向MyClass::OnDone中添加额外的参数时却会中断。发生了什么?
std::bind不只是像许多C++工程师期待的那样绑定前N个参数(这称之为部分函数应用)。你必须改为指定每个参数,因此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(absl::Status)> cb);
class MyClass {
void Start() {
DoStuffAsync(std::bind(&MyClass::OnDone, this));
}
void OnDone(); // 此处没有absl::Status
};
OnDone()不带参数,DoStuffAsync()回调应该接受absl::Status。你可能预期此处有一个编译错误,但是此代码实际上编译没有警告,因为std::bind过度弥补了空白。来自DoStuffAsync()的潜在错误会被默默忽略。
像这样的代码可能会造成严重的损坏。认为某些IO成功了,而实际上并没有,这可能是毁灭性的。也许MyClass的作者没有意识到DoStuffAsync()可能产生一个需要处理的错误。或者也许DoStuffAsync()曾经被用作接受std:function<void()>然后它的作者决定引入错误模式并更新所有停止编译的调用者。无论哪种方式,错误都会进入生产代码。
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()不会将绑定的只移动参数移动到目标函数。只需要使用absl::bind_front()替换std::bind()即可修复问题。
这下一个示例甚至经常绊倒C++专家。来看看你是否能够找到问题所在。
// F必须能够无参数调用
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(&MyClass::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()应用于你无法控制的类型始终是一个问题。DostuffAsync()不应该将std::bind()应用于模板。absl::bind_front()或lambda表达式都可以正常工作。
DoStuffAsync()的作用甚至可能有完全的绿色测试,因为它们总是传递一个lambda或std::function()作为参数,但是绝不会传递std::bind()的结果。MyClass的作者在遇到这个问题时会感到困惑。
std::bind()的这种特殊情况有用吗?并不真地有用。它只会碍手碍脚。如果你通过写嵌套的std::bind()调用来组合函数,那么你确实应该编写一个lambda或有名函数来代替。
希望你能够确信std::bind()是容易出错的。运行时或编译时的陷阱,对于新手和专家来说都是容易陷入的。现在我将展示,尽管在正确地使用std::bind(),这里依然还有一个更加可读的选项。
调用不带占位符的std::bind()最好用lambda表达式。
std::bind(&MyClass::OnDone, this)
相对于
[this]() { OnDone(); }
调用执行部分应用的std::bind最好使用absl::bind_fron()。你有的占位符越多,你获取的效果越明显。
std::bind(&MyClass::OnDone, this, std::placeholders::_1)
相对于
absl::bind_front(&MyClass::OnDone, this)
(在执行部分函数应用时,是使用absl::bind_front()还是lambda表达式是一种主观判断;请自行决定。)
这涵盖了所有调用std::bind()的99%的情况。剩余的调用只是做一些花哨的事情。
- 忽略一些参数: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++”,第34项:首选lambdas而非std::bind。
以上是关于本周小贴士#108:避免std::bind的主要内容,如果未能解决你的问题,请参考以下文章