Google C++每周贴士 #187: std::unique_ptr必须被移动

(原文链接:https://abseil.io/tips/187 译者:clangpp@gmail.com)

每周贴士 #186: std::unique_ptr必须被移动

如果你说第一幕中有个std::unique_ptr挂在墙上,那么在第二幕或者第三幕中它必须被移动,不然就没必要挂在那。——对不住啦契诃夫

std::unique_ptr是用来表达所有权转移的。如果你不需要转移所有权到别处,那std::unique_ptr这层抽象基本既没用又不合适。

std::unique_ptr是个啥?

std::unique_ptr是个指针,并且在std::unique_ptr析构的时候释放掉它指向的对象。它的存在就是为了用类型系统表达所有权(释放资源的责任),这也是C++11最有价值的增量之一1。然而,std::unique_ptr常常被过度使用。一个立见分晓的检验办法是:如果它从没被std::move过,那么它基本上不该是个std::unique_ptr 如果不转移所有权,那么大多数情况下都有比使用std::unique_ptr更好的表达意图的方式。

std::unique_ptr的代价

不需要转移所有权的时候,有一些避免使用std::unique_ptr的原因:

  • 我们应该使用能最贴切地表达语义的类型。std::unique_ptr代表可转移的所有权,在不需要转移所有权的时候是多此一举。
  • std::unique_ptr可以为空。如果空状态没被用到,那给读者徒增认知烦恼。
  • std::unique_ptr管理堆上申请的T,这会影响性能:既有堆操作自身的代价,又包括数据分散在堆上所导致的降低数据进入CPU缓存的概率的损失。

常见的反模式(Anti-Pattern):避免&

常常看到如下的例子:

int ComputeValue() {
  auto data = absl::make_unique<Data>();
  ModifiesData(data.get());
  return data->GetValue();
}

在这个例子里,data不需要是std::unique_ptr,因为所有权从没转移。如果把它声明为栈上的Data对象,其构造和析构的位置都不带变的。因此,就像Tip #123也讨论过的那样,更好的选项是:

int ComputeValue() {
  Data data;
  ModifiesData(&data);
  return data.GetValue();
}

常见的反模式:延迟初始化

因为std::unique_ptr默认初始化为空,而且可以被absl::make_unique赋新值,所以常常见到std::unique_ptr被用来做延迟初始化的途径。这个模式特别常见于GoogleTest,在那里测试夹具(test fixtures)允许在SetUp里初始化对象。

class MyTest : public testing::Test {
 public:
  void SetUp() override {
    thing_ = absl::make_unique<Thing>(data_);
  }

 protected:
  Data data_;
  // 在`SetUp`中初始化,因此我们用`std::unique_ptr`作为延迟初始化的途径。
  std::unique_ptr<Thing> thing_;
};

跟前面一样,我们注意到thing_的所有权从没转移到别的地方,所以没必要使用std::unqiue_ptr。上面的例子本可以在MyTest的默认构造函数里执行所有的初始化。更多关于SetUp和构造的比较请参考GoogleTest FAQ

class MyTest : public testing::Test {
 public:
  MyTest() : thing_(data_) {}

 private:
  Data data_;
  Thing thing_;
};

在这个例子中,data_跟以前一样是默认构造的。然后,Thingdata_构造得来。请记住,类的构造函数按照数据成员的声明顺序构造它们,所以这种方式初始化对象的顺序和以前一样,只是去掉了std::unique_ptr

如果延迟初始化真的重要且不可避免,请考虑使用absl::optional和它的emplace()方法。Tip #123更深入地讨论了延迟初始化。

class MyTest : public testing::Test {
 public:
  MyTest() {
    Initialize(&data_);
    thing_.emplace(data_);
  }

 private:
  Data data_;
  absl::optional<Thing> thing_;
};

这可是C++,当然会有特殊情况:就算对象从没被移动过,使用std::unique_ptr也是合理的。但是这样的情况并不常见,并且任何处理此种情况的代码都应该用注释小心解释清楚。下面有两个这样的例子。

巨大,不常用的对象

如果一个对象只是有时候会用到,absl::optional是个不错的默认选项。然而,不管对象有没有真的被构造,absl::optional都会预先申请空间。如果空间很重要,也许使用std::unique_ptr且仅在需要时申请空间会是个合理的选择。

祖传API(Legacy APIs)

一些祖传API会返回裸指针指向拥有的数据(owned data)(译者注:需要接收者负责释放)。这些API通常是在std::unique_ptr被引入C++标准库之前写的,这种模式不应该在新代码中被复制。然而,就算结果对象从没被移动过,这些祖传API调用也应该包裹一层std::unique_ptr,以保证内存不会被泄露。

Widget *CreateLegacyWidget() { return new Widget; }

int func() {
  Widget *w = CreateLegacyWidget();
  return w->num_gadgets();
}  // 内存泄露!

给对象包裹一层std::unique_ptr解决了以上所有问题:

int func() {
  std::unique_ptr<Widget> w(CreateLegacyWidget());
  return w->num_gadgets();
}  // `w`被正确地析构了

  1. std::unique_ptr以“unique”(译者注:独有)命名就是为了说明没有其他的std::unique_ptr应该持有相同的非空值。也就是说,在程序执行的任意时刻,在所有非空的std::unique_ptr中,所有std::unique_ptr持有的地址都各不相同。 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值