(原文链接:https://abseil.io/tips/187 译者:clangpp@gmail.com)
每周贴士 #186: std::unique_ptr
必须被移动
- 最初发布于:2020-11-05
- 作者:Andy Soffer
- 更新于:2020-11-05
- 短链接:abseil.io/tips/187
如果你说第一幕中有个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_
跟以前一样是默认构造的。然后,Thing
由data_
构造得来。请记住,类的构造函数按照数据成员的声明顺序构造它们,所以这种方式初始化对象的顺序和以前一样,只是去掉了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`被正确地析构了
std::unique_ptr
以“unique”(译者注:独有)命名就是为了说明没有其他的std::unique_ptr
应该持有相同的非空值。也就是说,在程序执行的任意时刻,在所有非空的std::unique_ptr
中,所有std::unique_ptr
持有的地址都各不相同。 ↩︎