C ++中make_shared和普通shared_ptr的区别

时间:2014-01-03 02:44:39

标签: c++ c++11 shared-ptr

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

很多google和stackoverflow帖子都在这上面,但我无法理解为什么make_shared比直接使用shared_ptr更有效率。

有人可以逐步解释我创建的对象序列和两者所做的操作,这样我就能理解make_shared的效率。我在上面举了一个例子供参考。

8 个答案:

答案 0 :(得分:289)

区别在于std::make_shared执行一次堆分配,而调用std::shared_ptr构造函数执行两次。

堆分配在哪里发生?

std::shared_ptr管理两个实体:

  • 控制块(存储诸如引用计数,类型擦除删除等元数据)
  • 正在管理的对象

std::make_shared对控制块和数据所需的空间执行单个堆分配计算。在另一种情况下,new Obj("foo")为托管数据调用堆分配,std::shared_ptr构造函数为控制块执行另一个分配。

有关详细信息,请查看cppreference上的实施说明

更新I:异常 - 安全

由于OP似乎对事物的异常安全方面感到疑惑,我已经更新了我的答案。

考虑这个例子,

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<Lhs>
  4. std::shared_ptr<Rhs>
  5. 现在,假设我们在步骤2中抛出异常(例如,内存不足异常,Rhs构造函数引发了一些异常)。然后我们丢失了在步骤1中分配的内存,因为没有任何东西有机会清理它。这里问题的核心是原始指针没有立即传递给std::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"));
    

    更新II:std::make_shared

    的缺点

    引用Casey的评论:

      

    由于只有一个分配,因此在控制块不再使用之前,不能释放指针的内存。 weak_ptr可以使控制块无限期地保持活动状态。

    为什么weak_ptr的实例会使控制块保持活动状态?

    weak_ptr必须有办法确定托管对象是否仍然有效(例如lock)。他们通过检查拥有托管对象的shared_ptr的数量来执行此操作,该shared_ptr s存储在控制块中。结果是控制块处于活动状态,直到weak_ptr计数和std::make_shared计数均为0。

    返回std::make_shared

    由于shared_ptr为控制块和托管对象进行单个堆分配,因此无法独立释放控制块和托管对象的内存。我们必须等到我们可以释放控制块和托管对象,这恰好是没有weak_ptrnew s活着。

    假设我们通过shared_ptrshared_ptr构造函数为控制块和托管对象执行了两次堆分配。然后,当没有weak_ptr s存活时,我们释放托管对象的内存(可能更早),并且当没有{{1}}生存时,释放控制块的内存(可能更晚)。 / p>

答案 1 :(得分:19)

共享指针管理对象本身以及包含引用计数和其他内务处理数据的小对象。 make_shared可以分配一个内存块来保存这两个内存;从指向已分配对象的指针构造共享指针需要分配第二个块来存储引用计数。

除了这个效率之外,使用make_shared意味着您根本不需要处理new和原始指针,从而提供更好的异常安全性 - 之后不可能抛出异常在将对象分配给智能指针之前分配对象。

答案 2 :(得分:15)

还有另一种情况,除了已经提到的两种可能性之外,如果你需要调用非公共构造函数(受保护或私有),make_shared可能无法访问它,而变量带有新作品很好。

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};

答案 3 :(得分:3)

如果在shared_ptr控制的对象上需要特殊的内存对齐,则不能依赖make_shared,但我认为这是不使用它的唯一理由。

答案 4 :(得分:2)

Shared_ptr:执行两次堆分配

  1. 控制块(引用计数)
  2. 正在管理的对象
  3. Make_shared:仅执行一次堆分配

    1. 控制块和对象数据。

答案 5 :(得分:0)

关于分配的效率和关注时间,我在下面做了这个简单的测试,我通过这两种方式创建了很多实例(一次一个):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

问题是,与使用new相比,使用make_shared需要花费两倍的时间。因此,使用new有两个堆分配,而不是使用make_shared。也许这是一个愚蠢的测试,但它不表明使用make_shared需要比使用new更多的时间吗?当然,我说的只是时间使用。

答案 6 :(得分:0)

我看到std :: make_shared存在一个问题,它不支持私有/受保护的构造函数

答案 7 :(得分:0)

我认为mpark先生的回答中的例外安全性仍然是一个令人担忧的问题。当创建这样的shared_ptr时:shared_ptr (new T),新的T可能会成功,而shared_ptr的控制块分配可能会失败。在这种情况下,新分配的T将泄漏,因为shared_ptr无法得知它是就地创建的,可以安全地删除它。还是我错过了什么?我认为对于函数参数求值的更严格规则在这里没有任何帮助...