Effective Modern C++ Item 21

Prefer std::make_unique and std::make_shared to direct use of new

一般情况下, 更倾向于使用make系列函数来创建智能指针,而不是直接使用new

首先, 它避免了代码重复。使用new的版本重复了类型,但是make函数的版本没有。

1
2
3
4
auto upw1(std::make_unique<Widget>());      //使用make函数
std::unique_ptr<Widget> upw2(new Widget); //不使用make函数
auto spw1(std::make_shared<Widget>()); //使用make函数
std::shared_ptr<Widget> spw2(new Widget); //不使用make函数

其次, make 系列函数有助于实现更好的异常安全代码。考虑以下代码:

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

processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); //潜在的资源泄漏!

在运行时,必须执行以下操作,processWidget才开始执行:

  • 一个Widget对象必须在堆上被创建
  • std::shared_ptr<Widget>构造函数执行
  • 运行computePriority()

以上三个操作可以按任意顺序执行。但如果正好按以顺序执行,computePriority() 抛出异常, 第一步动态分配的Widget就会泄漏。

使用std::make_shared可以防止这种问题:

1
processWidget(std::make_shared<Widget>(),  computePriority()); //没有潜在的资源泄漏    

第三,使用std::make_shared能提升效率。

直接使用new需要为Widget进行两次内存分配,一次为Widget对象,一次为控制块。

1
std::shared_ptr<Widget> spw(new Widget);

使用std::make_shared只分配一块内存,同时容纳了Widget对象和控制块。

1
auto spw = std::make_shared<Widget>();

但是有些情况下不适用std::make_shared。例如,make函数都不允许指定自定义删除器。如果想为智能指针指定一个自定义删除器, 只能直接使用 new

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函数中,完美转发使用小括号,而不是花括号。如果想用花括号初始化指向的对象,就必须使用new

1
2
3
4
5
6
7
// a vector of 10 elements, each equal to 20
auto spv = std::make_shared<std::vector<int>>(10, 20);

//折中方案,创建std::initializer_list
auto initList = { 10, 20 };
//使用std::initializer_list为形参的构造函数创建std::vector
auto spv = std::make_shared<std::vector<int>>(initList);

对于std::shared_ptr和它的make函数,还有2个问题。

第一是一些重载了operator newoperator delete的类。他们不太适用于std::shared_ptr对自定义分配和释放的支持。

第二是分配的对象非常大并且有 std::weak_ptr 指向它。通过std::shared_ptrmake函数分配的内存(对象+控制快),直到最后一个std::shared_ptr和最后一个指向它的std::weak_ptr都下降到 count=0 时,才会释放控制快,从而释放内存。这不适用于系统内存很吃紧的情况。

如果处于不合适使用std::make_shared的情况下,最好的方法是确保在直接使用new时,在一个不做其他事情的语句中,立即将结果传递到智能指针构造函数。防止在使用new和调用管理new出来对象的智能指针的构造函数之间发生异常。

1
2
3
4
processWidget(std::shared_ptr<Widget>(new Widget, cusDel),  computePriority());

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority()); //高效且异常安全

重点

  • 相比于直接使用new, std::make_uniquestd::make_shared可以消除代码重复, 提高异常安全性且生成的代码更小更快。

  • 不适合使用std::make_X系列函数的情况包括需要指定自定义删除器和希望用花括号初始化。

  • 对于 std::shared_ptr, 其他不建议使用make 系列函数的情况还包括 (1) 具有自定义内存管理的类 (2) 有内存担忧的系统(3) 非常大的对象、(4)比相应的 std::shared_ptr 生命周期更长的的 std::weak_ptr