每周技巧 #108:避免 `std::bind`
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #108: Avoid std::bind。
原文最初作为 TotW #108 发布于 2016 年 1 月 7 日。
更新于 2020 年 8 月 19 日。
避免 std::bind
本技巧总结了写代码时应该远离 std::bind() 的原因。
正确使用 std::bind() 很难。我们来看几个例子。下面这段代码看起来好吗?
|
|
许多经验丰富的 C++ 工程师都写过类似代码,结果发现它无法编译。它可以配合 std::function<void()> 工作,但当你给 MyClass::OnDone 增加一个额外参数时就坏了。怎么回事?
std::bind() 并不是像许多 C++ 工程师预期的那样,只绑定前 N 个参数(这叫偏函数应用)。你必须指定每个参数,所以正确的 std::bind() 咒语是:
|
|
呃,真难看。有更好的方式吗?当然有,请改用 std::bind_front()。(对于还不能使用 C++20 的代码,有 absl::bind_front。)
|
|
还记得偏函数应用吗,也就是 std::bind() 没有做的那件事?std::bind_front() 做的正是它:绑定前 N 个参数,并完美转发其余参数:std::bind_front(F, a, b)(x, y) 求值为 F(a, b, x, y)。
啊,理智恢复了。现在想看点真正吓人的东西吗?下面这段代码做什么?
|
|
OnDone() 不接收参数,而 DoStuffAsync() 的回调应该接收一个 absl::Status。你可能预期这里会有编译错误,但这段代码实际上会无警告编译,因为 std::bind 过度积极地填平了差距。来自 DoStuffAsync() 的潜在错误会被静默忽略。
这样的代码可能造成严重损害。以为某些 IO 成功了,而实际上没有,可能是灾难性的。也许 MyClass 的作者没有意识到 DoStuffAsync() 可能产生需要处理的错误。或者也许 DoStuffAsync() 过去接收 std::function<void()>,后来它的作者决定引入错误模式,并更新了所有停止编译的调用方。不管怎样,这个 bug 进入了生产代码。
std::bind() 禁用了我们都依赖的一个基本编译期检查。 编译器通常会告诉你调用方传入的参数是否超过预期,但对 std::bind() 不会。够吓人了吗?
另一个例子。你怎么看这个?
|
|
经典的跨异步边界传递 std::unique_ptr。不用说,std::bind() 不是解法:这段代码无法编译,因为 std::bind() 不会把绑定的只可移动参数移动到目标函数。简单地把 std::bind() 换成 std::bind_front() 就能修复。
下一个例子经常绊倒 C++ 专家。看看你能不能找到问题。
|
|
这无法编译,因为把 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()。std::bind_front() 或 lambda 都可以很好工作。
DoStuffAsync() 的作者甚至可能有全绿测试,因为他们总是传 lambda 或 std::function 作为实参,却从未传过 std::bind() 的结果。MyClass 的作者遇到这个 bug 时会很困惑。
std::bind() 的这个特殊情况有用吗?其实没有。它只会挡路。如果你通过编写嵌套 std::bind() 调用来组合函数,你真的应该改写成 lambda 或具名函数。
希望你已经相信,std::bind() 很容易用错。运行时和编译期陷阱对新人和 C++ 专家都很容易踩。现在我再说明,即使 std::bind() 被正确使用,通常也有另一个更可读的选项。
不带 placeholders 的 std::bind() 调用更适合写成 lambda。
|
|
对比:
|
|
执行偏函数应用的 std::bind() 调用更适合写成 std::bind_front()。 placeholders 越多,这一点越明显。
|
|
对比:
|
|
(执行偏函数应用时使用 std::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 或 std::bind_front。
扩展阅读
《Effective Modern C++》条款 34:优先使用 lambda,而不是 std::bind。