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

(原文链接: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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值