Item 21: Prefer std::make_unique and std::make_shared to direct use of new.
std::make_shared 是 C++11 开始支持的,但是 std::make_unique 是 C++14 才开始支持。如果你的编译器只支持 C++11,你可以实现自己的 make_unique。
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
std::make_unique 和 std::make_shared 是三个 make 函数中的两个,第三个 make 函数是 std::allocate_shared。它的行为和std::make_shared 一样,唯一的不同是它的第一个参数是一个分配器(allocator)对象,这个对象是用来动态申请内存的。make 函数能传入任意集合的参数,然后完美转发给构造函数,并动态创建一个对象,然后返回指向这个对象的智能指针。
创建智能指针有两种方式,一种是使用 make 函数,另一种是使用 new 直接创建。下面介绍二者的优缺点,并建议尽可能使用 make 函数。
make 函数的优点
支持 auto
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);
auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);
使用 make 函数的第一个优点是支持 auto,避免重复代码,使得代码更加清晰好维护。
避免异常
使用 make 函数的第二个优点跟异常安全有关。先看下面这个例子:
void processWidget(std::shared_ptr<Widget> spw, int priority);
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
processWidget(std::make_shared<Widget>(), computePriority());
如果使用 new,processWidget 调用时,产生如下步骤:
- 执行 new Widget
- 执行 std::shared_ptr 的构造
- 执行 computePriority()
但是,编译器可能不一定产生上述代码顺序。new Widget 肯定时要在 std::shared_ptr 的构造函数之前执行,但 computePriority() 可能在这两个步骤的前、中或后产生,可能时这样:
- 执行 new Widget
- 执行 computePriority()
- 执行 std::shared_ptr 的构造
如果 computePriority() 产生异常,第一步 new 的 Widget 还未被 std::shared_ptr 接管,会产生内存泄漏。使用 make 函数则不会有这样的问题。
效率更高
使用 make 函数的第三个优点是可以避免多次内存分配、效率更高。
std::shared_ptr<Widget> spw(new Widget);
auto spw = std::make_shared<Widget>();
使用 new,需要分配两次内存,一次分配 Widget 的内存,一次分配控制块的内存。若使用 make 函数,则只需要分配一次内存块,make 函数(std::shared_ptr 和 std::allocate_shared)会申请一块内存同时存储 Widget 和控制块。
make 函数的缺陷
上面介绍了 make 函数的优点,下面介绍 make 函数的缺陷。
无法自定义 deleter
使用 new,可以自定义 deleter,但是 make 函数无法做到。
auto widgetDeleter = [](Widget* pw) { … };
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
语义歧义
对于 std::vector,支持使用圆括号和花括号两种初始化方法:
std::vector<int> p(10, 20);
std::vector<int> p2{10, 20};
但是,make 函数不支持花括号的形式。原因是圆括号支持完美转发,花括号不支持完美转发,使用 make 函数可以完美转发圆括号。如果你想使用花括号进行初始化,只能使用 new。
auto sp1 = std::make_shared<std::vector<int>>(10, 20);
std::shared_ptr<std::vector<int>> sp2(new std::vector{10,20});
但是,Item 30 将会给出一个变通方案:使用auto类型推导来从初始化列表创建一个 std::initializer_list 对象,然后传入 auto 创建的对象给 make 函数:
auto initList = { 10, 20 };
auto spv = std::make_shared<std::vector<int>>(initList);
延长对象销毁时间
对于 make_shared_ptr ,它是申请一块内存块,用于储存对象和控制块。我们知道,创建 shared_ptr 时候会附属产生 weak_ptr, 它也有一个引用计数(weak 计数)存储在控制块中。
std::weak_ptr 是通过检查控制块中的引用计数(非 weak counter)判断自己是否失效。如果引用计数为 0,则 weak_ptr 失效,否则未失效。但是,只有 weak counter 不为 0,整个控制块就必须存在,那么 shared_ptr 指向的对象也不能释放。如果对象类型很大,并且最后一个 std::shared_pt r和最后一个 std::weak_ptr 销毁的间隔很大,那么一个对象销毁将延迟到最后才能释放。
class ReallyBigType { … };
auto pBigObj = std::make_shared<ReallyBigType>();
…
…
…
…
如果使用 new,因为是两块内存块,只要最后一个指向 ReallyBigType 对象的 std::shared_ptr 销毁了,这个对象的内存就能被释放:
class ReallyBigType { … };
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
…
…
…
…
一个 trick
讲完 make 的优缺点,我们回顾下上面说过的一个使用 new 可能导致内存泄漏的问题:
void processWidget(std::shared_ptr<Widget> spw, int priority);
void cusDel(Widget *ptr);
processWidget(std::shared_ptr<Widget>(new Widget, cusDel),
computePriority());
如果修改如下:
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());
这样可以避免内存泄漏,但是效率不高。可能存在异常泄漏的版本,我们传递给 processWidget 的是一个右值,而上面这个安全版本传递的是左值。传递右值只需要 move,而传递左值必须要拷贝,拷贝一个 std::shared_ptr 要求对它的引用计数进行一个原子的自增操作,但是 move 一个 std::shared_ptr 不需要修改引用计数。因此,上面的安全版本可以通过 move 来优化:
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority());
这样,使用 new,既安全又没有性能损失,并且还支持自定义 deleter。
最后,还是建议优先使用 make 函数,除非你有特殊的原因。
|