每周技巧 #126:`make_unique` 是新的 `new`
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #126: make_unique is the new new。
原文最初作为 totw/126 发布于 2016 年 12 月 12 日。
作者:James Dennett,基于 Titus Winters 的一篇邮件列表帖子整理。
快捷链接:abseil.io/tips/126
随着代码库扩张,要了解所有依赖细节会越来越困难。要求深度了解所有东西无法扩展:无论是写代码还是评审代码,我们都必须依赖接口和契约来判断代码正确。在许多情况下,类型系统可以用统一方式提供这些契约。持续使用类型系统契约,可以标识那些对堆分配对象存在潜在风险的分配或所有权转移位置,从而让代码编写和评审更容易。
在 C++ 中,虽然可以通过普通值减少动态内存分配需求,但有时我们需要对象活得比其作用域更久。动态分配对象时,C++ 代码应优先使用智能指针(最常见的是 std::unique_ptr),而不是裸指针。这围绕分配和所有权转移提供了一套一致叙事,也在可能需要更仔细检查所有权问题的代码位置留下更清晰的视觉信号。它还符合 C++14 之后外部世界的分配方式,并且具备异常安全性,这些都只是锦上添花。
这里有两个关键工具:absl::make_unique()(C++14 std::make_unique() 的 C++11 实现,用于无泄漏动态分配)和 absl::WrapUnique()(用于把拥有所有权的裸指针包装成对应的 std::unique_ptr 类型)。它们位于 absl/memory/memory.h。
为什么避免 new?
为什么代码应该优先使用智能指针和这些分配函数,而不是裸指针和 new?
-
只要可能,所有权最好通过类型系统表达。这允许评审者几乎完全通过局部检查验证正确性(没有泄漏,也没有重复删除)。(在极端性能敏感的代码中,这一点可以例外:虽然开销很小,但由于 ABI 约束,按值跨函数边界传递
std::unique_ptr仍有非零开销。不过这种开销很少重要到足以证明避免它是合理的。) -
有点类似于优先使用
push_back()而不是emplace_back()(TotW 112)的理由,absl::make_unique()直接表达意图,而且只能做一件事(用公开构造函数进行分配,并返回指定类型的std::unique_ptr)。这里没有类型转换或隐藏行为。absl::make_unique()名副其实。 -
用
std::unique_ptr<T> my_t(new T(args));也可以达到同样效果,但这很冗余(重复了类型名T),而且对一些人来说,尽量减少对new的调用本身也有价值。第 5 点会进一步讨论。 -
如果所有分配都通过
absl::make_unique()或工厂调用处理,那么absl::WrapUnique()就留给这些工厂调用的实现、与不依赖std::unique_ptr进行所有权转移的旧方法交互的代码,以及需要用聚合初始化进行动态分配的少见情况(absl::WrapUnique(new MyStruct{3.141, "pi"}))。在代码评审中,absl::WrapUnique调用很容易被发现,并评估“这个表达式看起来像所有权转移吗?”通常答案很明显(例如它是某个工厂函数)。如果不明显,我们就需要检查该函数,确认它确实是在转移裸指针所有权。 -
如果我们转而主要依赖
std::unique_ptr的构造函数,会看到这样的调用:
std::unique_ptr<T> foo(Blah());
std::unique_ptr<T> bar(new T());
稍微检查一下就能看出后者是安全的(没有泄漏,没有重复删除)。前者呢?要看情况:如果Blah()返回的是std::unique_ptr,那没问题;不过这种情况下写成下面这样会更明显安全:
std::unique_ptr<T> foo = Blah();
如果Blah()返回的是转移所有权的裸指针,也没问题。如果Blah()返回的只是某个随机指针(没有转移所有权),那就有问题。依赖absl::make_unique()和absl::WrapUnique()(避免直接使用构造函数)会给那些需要担心的位置提供额外视觉线索:也就是absl::WrapUnique()调用,而且只有这些调用。
应该如何选择使用哪一个?
-
默认情况下,动态分配使用
absl::make_unique()(或者在少数共享所有权合适的场景中使用std::make_shared())。例如,不要写std::unique_ptr<T> bar(new T());,而是写auto bar = absl::make_unique<T>();;不要写bar.reset(new T());,而是写bar = absl::make_unique<T>();。 -
在使用非公开构造函数的工厂函数中,返回
std::unique_ptr<T>,并在实现中使用absl::WrapUnique(new T(...))。 -
动态分配需要花括号初始化的对象(通常是结构体、数组或容器)时,使用
absl::WrapUnique(new T{...})。 -
调用通过
T*接收所有权的旧 API 时,要么提前用absl::make_unique分配对象,并在调用中使用ptr.release(),要么直接在函数实参中使用new。 -
调用通过
T*返回所有权的旧 API 时,立即用WrapUnique构造智能指针(除非你会立刻把这个指针传给另一个通过T*接收所有权的旧 API)。
总结
优先使用 absl::make_unique() 而不是 absl::WrapUnique(),并优先使用 absl::WrapUnique() 而不是裸 new。