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>());       // 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 函数的第一个优点是支持 auto,避免重复代码,使得代码更加清晰好维护。

避免异常

使用 make 函数的第二个优点跟异常安全有关。先看下面这个例子:

void processWidget(std::shared_ptr<Widget> spw, int priority);  // declare

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

如果使用 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);   // 10 elements, every element is 20
std::vector<int> p2{10, 20};  // two elements: 10 and 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 函数:

// create std::initializer_list
auto initList = { 10, 20 };
// create std::vector using std::initializer_list ctor
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>();  // create very large object via std::make_shared// create std::shared_ptrs and std::weak_ptrs to large object, use them to work with it// final std::shared_ptr to object destroyed here, but std::weak_ptrs to it remain// during this period, memory formerly occupied by large object remains allocated// final std::weak_ptr to object destroyed here;  memory for control block and object is released

如果使用 new,因为是两块内存块,只要最后一个指向 ReallyBigType 对象的 std::shared_ptr 销毁了,这个对象的内存就能被释放:

class ReallyBigType {}; // as before
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);  // create very large object via new// as before, create std::shared_ptrs and std::weak_ptrs to object, use them with it// final std::shared_ptr to object destroyed here, but std::weak_ptrs to it remain; memory for object is deallocated// during this period, only memory for the control block remains allocated// final std::weak_ptr to object destroyed here; memory for control block is released

一个 trick

讲完 make 的优缺点,我们回顾下上面说过的一个使用 new 可能导致内存泄漏的问题:

void processWidget(std::shared_ptr<Widget> spw, int priority);  // as before
void cusDel(Widget *ptr); // custom deleter

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

如果修改如下:

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());  // correct, but not optimal; see below

这样可以避免内存泄漏,但是效率不高。可能存在异常泄漏的版本,我们传递给 processWidget 的是一个右值,而上面这个安全版本传递的是左值。传递右值只需要 move,而传递左值必须要拷贝,拷贝一个 std::shared_ptr 要求对它的引用计数进行一个原子的自增操作,但是 move 一个 std::shared_ptr 不需要修改引用计数。因此,上面的安全版本可以通过 move 来优化:

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw),  computePriority()); // both efficient and exception safe

这样,使用 new,既安全又没有性能损失,并且还支持自定义 deleter。

最后,还是建议优先使用 make 函数,除非你有特殊的原因。

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值