C ++中make_shared和常规shared_ptr的区别


276
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效率。我在上面给出了一个例子供参考。


4
效率不高。使用它的原因是出于例外安全性。
Yuushi 2014年

有些文章说,它避免了一些建设开销,请您对此进行详细说明?
Anup Buchke 2014年

16
@Yuushi:异常安全是使用它的一个很好的理由,但是它也更有效。
Mike Seymour 2014年

3
32:15是他在我上面链接的视频中开始的地方,如果有帮助的话。
克里斯

4
次要代码风格的优势:使用make_shared您可以编写,auto p1(std::make_shared<A>())并且p1将具有正确的类型。
Ivan Vergiliev 2014年

Answers:


333

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

堆分配在哪里发生?

std::shared_ptr 管理两个实体:

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

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

欲了解更多信息,请查看执行音符cppreference

更新一:异常安全

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

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

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

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

更新二:的缺点 std::make_shared

引用凯西的评论:

由于只有一种分配,因此在不再使用控制块之前,无法释放指针对象的内存。A weak_ptr可以使控制块无限期地存活。

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

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

回到 std::make_shared

由于std::make_shared对控制块和托管对象都进行了一次堆分配,因此无法独立为控制块和托管对象释放内存。我们必须等到可以释放控制块和被管理对象为止,碰巧直到没有shared_ptrs或weak_ptrs存活为止。

假设我们改为通过newshared_ptr构造函数为控制块和托管对象执行了两次堆分配。然后,当不存在shared_ptrs时,为托管对象释放内存(可能更早),而当不存在s时,为控制块释放内存(可能更晚)weak_ptr


53
最好还提及一个小小的极端情况make_shared:由于只有一种分配,因此在不再使用控制块之前,无法释放指针对象的内存。A weak_ptr可以使控制块无限期地存活。
Casey

14
另一个更风格上的观点是:如果始终使用make_shared并且make_unique不会拥有原始指针,则可以将每次出现的情况都new视为代码气味。
菲利普2014年

6
如果只有一个shared_ptr,而没有weak_ptr,则调用reset()shared_ptr实例将删除控制块。但是,无论是否make_shared使用。使用make_shared会有所不同,因为它可以延长为托管对象分配的内存的寿命。当shared_ptr计数击中0,托管对象的析构函数获取不管叫make_shared,但是如果释放它的内存只能做make_shared没有使用。希望这使事情更加清楚。
mpark

4
还值得一提的是make_shared可以利用“我们知道您住的地方”优化,该优化允许控制块的指针变小。(有关详细信息,请参阅Stephan T. Lavavej在大约12分钟时的GN2012演示文稿。)make_shared因此不仅避免了分配,而且分配了更少的总内存。
KnowItAllWannabe 2014年

1
@HannaKhalil:这也许就是您要寻找的东西的境界吗?melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark

26

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

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


2
我正确理解了你的第一点。您能否详细说明或提供关于异常安全性的第二点的链接?
Anup Buchke 2014年

22

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

我遇到了这个确切的问题,决定使用new,否则我会使用make_shared。这是一个与此相关的问题:stackoverflow.com/questions/8147027/…
jigglypuff

6

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


2
make_shared不适当的第二种情况是您要指定自定义删除程序。
KnowItAllWannabe 2014年

5

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


3

Shared_ptr:执行两个堆分配

  1. 控制块(参考计数)
  2. 被管理对象

Make_shared:仅执行一个堆分配

  1. 控制块和对象数据。

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花费更多的时间吗?当然,我说的只是时间。


4
该测试没有意义。测试是否在发布配置中进行了优化?另外,您的所有项目都会立即释放,因此不现实。
Phil1970年

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.