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

许多谷歌和stackoverflow的帖子都在这里,但我不明白为什么make_shared比直接使用shared_ptr更有效。

有人能一步一步地向我解释创建的对象序列和两者所做的操作,这样我就能理解make_shared是如何高效的。我在上面举了一个例子供大家参考。


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

堆分配发生在哪里?

Std::shared_ptr管理两个实体:

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

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

有关更多信息,请参阅cppreference中的实现说明。

更新I:异常安全

注(2019/08/30):自c++ 17以来,由于函数参数的求值顺序的变化,这不是一个问题。具体来说,函数的每个参数都需要在对其他参数求值之前完全执行。

由于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++允许子表达式求值的任意顺序,一种可能的顺序是:

新Lhs(“foo”) 新Rhs(“酒吧”) std:: shared_ptr < Lhs > std:: shared_ptr < Rhs >

现在,假设我们在第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"));

更新二:std::make_shared的缺点

引用Casey的评论:

因为只有一次分配,所以只有控制块不再使用时才能释放被指针的内存。weak_ptr可以无限期地保持控制块处于活动状态。

为什么weak_ptrs实例保持控制块为活动状态?

weak_ptrs必须有一种方法来确定托管对象是否仍然有效(例如。锁)。它们通过检查拥有管理对象的shared_ptrs的数量来做到这一点,这些shared_ptrs存储在控制块中。结果是控制块在shared_ptr计数和weak_ptr计数都达到0之前都是活跃的。

回到std::make_shared

由于std::make_shared对控制块和管理对象进行了单独的堆分配,因此没有办法为控制块和管理对象独立释放内存。我们必须等待,直到我们可以释放控制块和托管对象,这恰好是直到没有活的shared_ptrs或weak_ptrs。

假设我们通过new和shared_ptr构造函数为控制块和托管对象执行两次堆分配。然后,当没有shared_ptrs活的时候,释放托管对象的内存(可能更早),当没有weak_ptrs活的时候,释放控制块的内存(可能更晚)。


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

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


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


在前面提到的情况之上,还有另一种情况是两种可能性不同的:如果需要调用非公共构造函数(受保护的或私有的),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){}
};

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

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));
}

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


Shared_ptr:执行两个堆分配

控制块(引用计数) 正在管理的对象

Make_shared:只执行一次堆分配

控制块和对象数据。


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

std::shared_ptr(new T(args…))如果在可访问的上下文中执行,可能会调用T的非公共构造函数,而std::make_shared需要对所选构造函数进行公共访问。

https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared#Notes


我认为mpark先生的回答中的异常安全部分仍然是一个合理的担忧。当像这样创建shared_ptr: shared_ptr< T >(新T)时,新T可能会成功,而shared_ptr分配控制块可能会失败。在这种情况下,新分配的T会泄漏,因为shared_ptr无法知道它是就地创建的,删除它是安全的。还是我遗漏了什么?我不认为更严格的函数参数计算规则在这里有任何帮助…