每周技巧 #187:`std::unique_ptr` 必须被移动
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #187: std::unique_ptr Must Be Moved。
原文最初作为 TotW #187 发布于 2020 年 11 月 5 日。
作者:Andy Soffer
更新于 2020 年 11 月 5 日。
快捷链接:abseil.io/tips/187
如果你在第一章说墙上挂着一个 std::unique_ptr,那么第二章或第三章它绝对必须被移动。如果它不会被移动,就不该挂在那里。~ 向 Anton Chekhov 致歉
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::move 过来,它很可能不应该是 std::unique_ptr。 如果我们不转移所有权,那么几乎总有比使用 std::unique_ptr 更好的方式表达意图。
std::unique_ptr 的成本
在不转移所有权时,有几个理由避免使用 std::unique_ptr。
std::unique_ptr表达可转移所有权;如果所有权并未转移,这个信息没有帮助。我们应当尽量使用最准确表达所需语义的类型。std::unique_ptr可以为空;如果空状态实际上没有使用,就会给读者增加额外认知负担。std::unique_ptr<T>管理堆分配的T,这会带来性能影响:既有堆分配本身的成本,也因为数据分散在堆上,更不容易位于 CPU 缓存中。
常见反模式:避免使用 &
经常可以看到下面这样的例子:
|
|
在这个例子中,data 不需要是 std::unique_ptr,因为所有权从未转移。Data 对象的构造和销毁时机,与在栈上声明一个 Data 对象完全相同。因此,正如技巧 #123 中也讨论过的,更好的选择是:
|
|
常见反模式:延迟初始化
因为 std::unique_ptr 默认构造时为空,并且可以从 std::make_unique 赋入新值,所以常常被用作延迟初始化机制。在 GoogleTest 中有一种特别常见的模式:测试夹具可以在 SetUp 中初始化对象。
|
|
我们再次看到,thing_ 的所有权从未转移到别处,因此没有必要使用 std::unique_ptr。上面的例子本可以在 MyTest 的默认构造函数中完成所有初始化。关于 SetUp 与构造的区别,见 GoogleTest FAQ。
|
|
在这个例子中,data_ 像之前一样默认构造。之后,Thing 用 data_ 构造。记住,类的构造函数按字段声明顺序初始化字段,因此这种方式与之前以相同顺序初始化对象,但不使用 std::unique_ptr。
如果延迟初始化确实重要且无法避免,可以考虑使用 std::optional 及其 emplace() 方法。技巧 #123 对延迟初始化有更深入讨论。
|
|
注意事项
既然这是 C++,当然存在即使从未移动也适合使用 std::unique_ptr 的场景。不过这些情况并不常见,处理此类情况的代码应该带有注释说明其中微妙之处。下面是两个例子。
很大且很少使用的对象
如果某个对象只在有时需要,std::optional 是一个不错的默认选择。不过,std::optional 无论对象是否真的构造,都会预留空间。如果这部分空间很重要,那么持有 std::unique_ptr 并只在需要时分配它,可能是合理的。
遗留 API
许多遗留 API 返回指向所拥有数据的原始指针。这些 API 往往早于 std::unique_ptr 加入 C++ 标准库;新代码不应该复制这种模式。不过,即使得到的对象永远不会移动,也应该把这类遗留 API 调用包装在 std::unique_ptr 中,确保内存不会泄漏。
|
|
用 std::unique_ptr 包装对象可以解决这两个问题:
|
|
-
std::unique_ptr名称中的 “unique” 被选来表示这样一个想法:不应有其他std::unique_ptr持有相同的非空值。也就是说,在程序执行的任意时刻,所有非空std::unique_ptr所持有的地址,在这些std::unique_ptr中都是唯一的。 ↩︎