本周小贴士#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的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#120:返回值是不可触碰的

本周小贴士#74:委托和继承构造函数

本周小贴士#88:初始化:=,()和{}

本周小贴士#116: Using声明和命名空间别名

本周小贴士#116: Using声明和命名空间别名

本周小贴士#93:使用absl::Span