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.

### C++ 中 `std::function` 和 `std::bind` 的用法及区别 #### 定义与基本概念 `std::function` 是一种通用多态函数封装器,可以存储任何可调用对象(如普通函数、lambda 表达式、绑定表达式或其他函数对象)。其灵活性使得它能够作为回调机制的一部分,在事件驱动编程和其他场景下非常有用。 ```cpp #include <functional> #include <iostream> void simpleFunction(int a) { std::cout << "Value is: " << a << &#39;\n&#39;; } int main() { // 创建一个 std::function 对象并赋值给简单函数 std::function<void(int)> f = simpleFunction; f(42); } ``` 另一方面,`std::bind` 提供了一种方式来创建新的可调用对象,这些新对象可以通过固定某些参数或将多个可调用实体组合在一起的方式简化现有可调用目标的接口。通过 bind 可以提前设置部分参数,从而减少后续调用时传递相同参数的需求[^1]。 ```cpp #include <functional> #include <iostream> class MyClass { public: void memberFunc(int i, double d) const { std::cout << "Integer value: " << i << ", Double Value:" << d << "\n"; } }; int main(){ using namespace std::placeholders; MyClass obj; // 绑定成员函数到特定实例,并预先设定第一个参数 auto boundMember = std::bind(&MyClass::memberFunc, &obj, _1, 3.14); // 调用已绑定的对象只需要提供剩余未固定的参数即可 boundMember(7); } ``` #### 主要差异点 - **目的同**: `std::function` 更侧重于作为一个容器去保存各种类型的可调用对象;而 `std::bind` 则专注于调整已有可调用对象的行为模式,比如预设一些输入参数。 - **使用场合的区别**: 当需要定义一个能接受多种同类型但签名兼容的函数指针的地方时,应该优先考虑使用 `std::function`; 如果想要改变某个具体函数或方法的工作方式,则可以选择 `std::bind`. - **性能考量**: 在大多数情况下,直接使用 lambda 表达式可能比使用 `std::bind` 效率更高,因为后者可能会引入额外的小型分配开销以及间接层。然而对于复杂情况下的参数绑定操作来说,`std::bind` 还是有它的优势所在[^2].
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值