智能指针之make_unique与make_shared

make_unique的实现

std::make_shared是C++11的一部分,但是std::make_unique很可惜不是。它是在C++14里加入标准库的,但我们可以自己实现make_unique方法。

// 支持普通指针
template<class T,class... Args> inline
typename enable_if<!is_array<T>::value,unique_ptr<T>>::type
make_unique(Args&&... args){
    return unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// 支持动态数组
template<class T> inline
typename enable_if<is_array<T>::value && extent<T>::value == 0,unique_ptr<T>>::type
make_unique(size_t size){
    typedef typename remove_extent<T>::type U;
    return unique_ptr<T>(new U[size]());
}

// 过滤掉定长数组的情况
template<class T,class... Args>
typename enable_if<extent<T>::value != 0,void>::type
make_unique(Args&&...) = delete;

enable_if的作用

// Primary template.
/// Define a member typedef @c type only if a boolean constant is true.
template<bool, typename _Tp = void>
  struct enable_if
  { };

// Partial specialization for true.
template<typename _Tp>
  struct enable_if<true, _Tp>
  { typedef _Tp type; };

结合源码可知,当condition==true时,enable_if<condition,T>::type ≡ T,否则报错。

  • enable_if<!is_array<T>::value,unique_ptr<T>>::typeconditionT不是数组类型时为true
  • enable_if<is_array<T>::value && extent<T>::value == 0,unique_ptr<T>>::typeconditionT为数组类型且数组中元素个数为0时为true,由于对于非数组类型extent<U>::value也为0,语句is_array<T>::value是必要的
  • enable_if<extent<T>::value != 0,void>::typeconditionT类型中元素个数不为0时为true,即T为定长数组

std::forward的作用

std::forward在这里的作用是实现参数的完美转发,具体见《move和forward源码分析[转]》

make函数的好处

1. 效率更高

shared_ptr需要维护引用计数的信息。如果你通过使用原始的new表达式分配对象,然后传递给shared_ptr(也就是使用shared_ptr的构造函数)的话,shared_ptr的实现没有办法选择,而只能单独的分配控制块:

这里写图片描述

如果选择使用make_shared的话,情况就会变成下面这样:

这里写图片描述

内存分配的动作,可以一次性完成。这减少了内存分配的次数,而内存分配是代价很高的操作。

2. 异常安全

看看下面的代码:

void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

C++是不保证参数求值顺序,以及内部表达式的求值顺序的,所以可能的执行顺序如下:

  1. new Lhs(“foo”))
  2. new Rhs(“bar”))
  3. std::shared_ptr
  4. std::shared_ptr

假设在第2步的时候,抛出了一个异常(比如out of memory,总之,Rhs的构造函数异常了),那么第一步申请的Lhs对象内存泄露了。这个问题的核心在于,shared_ptr没有立即获得裸指针。

我们可以用如下方式来修复这个问题:

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

当然,推荐的做法是使用std::make_shared来代替:

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

std::make_shared被调用,指向动态内存对象的原始指针会被安全的保存在返回的std::shared_ptr对象中,然后另一std::make_shared被调用。如果此时产生了异常,那std::shared_ptr析构会知道于是它所拥有的对象会被销毁。

使用std::make_unique来代替new在写异常安全的代码里和使用std::make_shared一样重要。

make函数的不足

  • make函数都不允许使用定制删除器,但是std::unique_ptrstd::shared_ptr的构造函数都可以。
  • make函数不能完美传递一个initializer_list
    替代方案:
// initializer_list<int> aa = {1,2,3}; // 或者
auto aa = {1,2,3};
auto a = make_shared<vector<int>>(aa);
// auto b = make_shared<vector<int>>({1,2,3}); // 错误
  • 对象的内存可能无法及时回收

虽然使用std::make_shared可以减少了内存分配的次数,提高效率,但由于控制块与对象都在同一块动态分配的内存上,所以当对象的引用计数变为0,对象被销毁(析构函数被调)后,该对象所占内存仍未释放,直到控制块同样也被销毁,内存才会释放。

我们知道,在控制块中包含两个计数:shared countweak count,分别表示std::shared_ptrstd::weak_ptr对对象的引用计数,只有当shared countweak count都为0时,控制块才会被销毁。

换句话说,只要有std::weak_ptr指向一个控制块(weak count大于0),那控制块就一定存在。只要控制块存在,包含它的内存必定存在。通过std::shared_ptrmake函数分配的内存在最后一个std::shared_ptr和最后一个std::weak_ptr被销毁前不能被释放。

  • 构造函数是保护或私有时,无法使用make_shared
    替代方案:
class A {
public:
    static std::shared_ptr<A> create() {
        return std::make_shared<A>();
    }
protected:
    A() {}
    A(const A &) = delete;
    const A &operator=(const A &) = delete;
};

std::shared_ptr<A> foo() {
    return A::create();
}

参考链接

c++11 条款21:尽量使用std::make_unique和std::make_shared而不直接使用new
Why Make_shared ?
通过new和make_shared构造shared_ptr的性能差异
How does weak_ptr work?
shared_ptr线程安全性分析
How do I call ::std::make_shared on a class with only protected or private constructors?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值