EMCPP条款21:优先选用std::make_unique和std::make_shared,而非直接使用new

YiQi 管理员

优先选用的场景

优先选用 make 系列函数的第一个原因,避免重复撰写型别

1
2
3
4
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
auto spw1(std::make_shared<Widget>()); // with make func
std::shared_ptr<Widget> spw2(new Widget); // without make func

优先使用 make 系列函数的第二个原因与异常安全有关。假设我们有一个函数依据某种优先级来处理一个 Widget 对象和一个函数用来计算相对优先级::

1
2
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();

我们在 processWidget 的调用中用到该函数,并且在这次调用中,processWidget 使用了 new 运算符而非 std::make_shared:

1
2
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
// potential resource leak!

代码在什么地方会发生资源泄漏呢?答案与编译器从源代码到目标代码的翻译有关。在运行期,传递给函数的实参必须在函数调用被发起之前完成评估求值。因此,在 processWidget 的调用过程中,下列事件必须在 processWidget 开始执行前发生:

  • 表达式 new Widget 必须先完成评估求值,即,一个 Widget 对象必须先在堆上创建。
  • new 产生的裸指针的托管对象 std::shared_ptr<Widget> 的构造函数必须执行。
  • computePriority 必须运行。

编译器不必按上述顺序来生成代码。编译器可能会放出这样的代码,以按如下时序执行操作:

  1. 实施 new Widget
  2. 执行 computePriority
  3. 运行 std::shared_ptr 构造函数。
    如果生成了这样的代码,并且在运行期 computePriority 产生了异常,那么由第一步动态分配的 Widget 会被泄漏,因为它将永远不会被存储到在第三步才接管的 std::shared_ptr 中去。

使用 std::make_shared 可以避免该问题。调用代码如下:

1
2
processWidget(std::make_shared<Widget>(), computePriority()); 
// no potential resource leak

在运行期,std::make_sharedcomputePriority 中肯定有一个会首先披调用。

如果我们把 std::shared_ptrstd::make_shared 分别替换成 std::unique_ptrstd::make_unique, 则推理过程完全相同。

std::make_shared 的另 一个特色(与直接使用 new 表达式相比),是性能的提升。使用 std::make_shared 会让编译器有机会利用更简洁的数据结构产生更小更快的代码。

不能使用的场景

本条款仍然主张的是优先选用 make 系列函数,而非排他性地使用之。原因在于,还是有一些情景之下,不能或者不应使用 make 系列函数。例如,所有的 make 系列函数都不允许使用自定义析构器

1
2
3
auto widgetDeleter = [](Widget* pw) { … };
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);

make 系列面数的第二个限制源于其实现的一个语法细节。 make 系列面数会向对象的构造函数完美转发其形参,但当它们到底是在使用圆括号的时候这样做,还是在使用大括号时这样做呢?对于某些型别而言,这个问题的答案会根据使用括号种类的不同而有很大不同。

有些类会定义自身版本的 opeator newoperator delete, 这些函数的存在意味着全局版本的内存分配和释放函数不适用于这种对象。因此,使用 make 系列函数去为带有自定义版本的 operate newoperator delete 的类创建对象,通常并不是个好主意。

下一个限制源于控制块和指涉对象的析构有关,参见原文

使用了自定义析构器的话,法使用 std::make_shared

要点速记

  • 相比于直接使用 new 表达式,make 系列函数消除了重复代码、改进了异常安全性,并且对于 std::make_sharedstd::allcoated_shared 而言,生成的目标代码会尺寸更小、速度更快。
  • 不适于使用 make 系列函数的场景包括需要定制删除器,以及期望直接传递大括号初始化物。
  • 对于 std::shared_ptr, 不建议使用 make 系列函数的额外场景包括
    • 自定义内存管理的类
    • 内存紧张的系统、非常大的对象、以及存在比指涉到相同对象的 std::shared_ptr 生存期更久的 std::weak_ptr
此页目录
EMCPP条款21:优先选用std::make_unique和std::make_shared,而非直接使用new