天天看点

Google C++每周贴士 #108: 避免使用std::bind每周贴士 #108: 避免使用std::bind

(原文链接:https://abseil.io/tips/108 译者:[email protected])

每周贴士 #108: 避免使用

std::bind

  • 最初发布于2016-01-07
  • 作者:Roman Perepelitsa ([email protected])
    • (译者注:这哥们儿是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()

正确的咒语是:

那个啥,真丑。有木有好点儿的方式?还真有,用

absl::bind_front()

还记得早前提到

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()

实现的偏函数不如改用

absl::bind_front()

。占位符越多,差距越明显。

对比

(实现偏函数的时候,用

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.

继续阅读