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

Anu*_*hke 252 c++ shared-ptr c++11

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));
Run Code Online (Sandbox Code Playgroud)

许多google和stackoverflow帖子就在这里,但我无法理解为什么make_shared比直接使用更有效shared_ptr.

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

mpa*_*ark 313

区别在于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")));
Run Code Online (Sandbox Code Playgroud)

因为C++允许子表达式的任意顺序评估,所以一种可能的排序是:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. 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);
Run Code Online (Sandbox Code Playgroud)

当然,解决这个问题的首选方法是使用std::make_shared.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
Run Code Online (Sandbox Code Playgroud)

更新II:缺点 std::make_shared

引用凯西的评论:

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

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

必须有一种方法让weak_ptrs来确定托管对象是否仍然有效(例如for lock).他们通过检查shared_ptr拥有托管对象的s 的数量来实现这一点,该托管对象存储在控制块中.结果是控制块处于活动状态,直到shared_ptr计数和weak_ptr计数都达到0.

回到 std::make_shared

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

假设我们代替执行的两个堆分配的控制块,并经由被管理对象newshared_ptr构造函数.然后,当没有shared_ptr生存时,我们释放托管对象的内存(可能更早),并且当没有生存时,释放控制块的内存(可能更晚)weak_ptr.

  • 提及`make_shared`的小角落缺点也是一个好主意:因为只有一个分配,指针的内存在控制块不再使用之前无法释放.`weak_ptr`可以使控制块无限期地保持活动状态. (49认同)
  • 另一个更具风格的观点是:如果你一直使用`make_shared`和`make_unique`,你将不会拥有原始指针,可以将每次出现的`new`视为代码气味. (12认同)
  • 如果只有一个`shared_ptr`,而没有`weak_ptr`,则在`shared_ptr`实例上调用`reset()`将删除控制块.但这无论是否使用`make_shared`.使用`make_shared`会产生影响,因为它可以延长__为托管对象分配的内存的生命周期.当`shared_ptr`计数达到0时,无论`make_shared`是什么,都会调用托管对象的析构函数,但只有在`make_shared`被__not__使用时才能释放它的内存.希望这更清楚. (4认同)
  • 还值得一提的是,make_shared可以利用"我们知道你住在哪里"的优化,允许控制块更小的指针.(有关详情,请参见[斯蒂芬T. Lavavej的GN2012介绍](http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/STL11-Magic-Secrets)在约12分钟)make_shared从而不仅避免了分配时,它还分配更少的总内存. (4认同)

Mik*_*our 21

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

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

  • 我正确理解了你的第一点。您能否详细说明或提供有关异常安全性的第二点的链接? (2认同)

Dr_*_*Sam 19

还有另一种情况,除了已经提到的两种可能性之外,如果你需要调用非公共构造函数(受保护或私有),make_shared可能无法访问它,而具有new的变体可以正常工作.

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){}
};
Run Code Online (Sandbox Code Playgroud)


小智 6

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

  • make_shared 不合适的第二种情况是当您要指定自定义删除器时。 (2认同)

小智 6

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