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

394
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为什么更高效。我已经给出了一个以上的例子供参考。


6
不是更高效。使用它的原因是为了异常安全性。 - Yuushi
2
STL在Channel 9的视频中涵盖了这个内容。可能是这个视频。 - chris
20
@Yuushi:异常安全性是使用它的一个很好的理由,但它也更有效率。 - Mike Seymour
4
如果有帮助的话,视频中他从32分15秒开始讲述。 - chris
5
代码风格小技巧:使用 make_shared 可以编写 auto p1(std::make_shared<A>()),p1 将具有正确的类型。 - Ivan Vergiliev
显示剩余2条评论
8个回答

465
区别在于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 起。具体来说,对函数的每个参数都需要在评估其他参数之前完全执行。
由于提问者似乎想了解异常安全方面的内容,我已更新我的答案。
考虑以下示例:
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"));

更新 II:使用 std::make_shared 的缺点

引用Casey的评论:

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

weak_ptr 实例是如何保持控制块存活的?

必须有一种方法让 weak_ptr 确定受管理对象是否仍然有效(例如用于 lock)。它们通过检查拥有所管理对象的 shared_ptr 数量来完成此操作,该数量存储在控制块中。结果是,控制块存活直至 shared_ptr 计数和 weak_ptr 计数都达到0。

回到 std::make_shared

由于 std::make_shared 为控制块和管理对象同时进行单个堆分配,因此没有办法独立释放控制块和管理对象的内存。我们必须等待直到没有 shared_ptrweak_ptr 存在时才能释放两者的内存。

假设我们改为通过 newshared_ptr 构造函数分别对控制块和管理对象进行两个堆分配。然后,当没有 shared_ptr 存在时,我们释放管理对象的内存(可能更早),并在没有 weak_ptr 存在时释放控制块的内存(可能更晚)。


74
提到 make_shared 的小角落缺点是个好主意:由于只有一个分配,指针的内存一直不能释放,直到控制块不再使用。weak_ptr 可以无限期地保持控制块处于活动状态。 - Casey
25
另一个更加注重风格的观点是:如果您一致使用make_sharedmake_unique,就不会有拥有原始指针的情况出现,也可以将每个new出现视为代码异味。 - Philipp
6
如果只有一个shared_ptr,且没有weak_ptr,在shared_ptr实例上调用reset()将删除控制块。但这无论是否使用了make_shared都是如此。使用make_shared会产生区别,因为它可以延长__分配给托管对象的内存__的生命周期。当shared_ptr计数达到0时,无论是否使用make_shared,都会调用托管对象的析构函数,但只有在未使用make_shared时才能释放其内存。希望这样更清楚明白。 - mpark
4
值得一提的是,make_shared可以利用“我们知道你住在哪里”优化,使控制块成为一个更小的指针。(详情请参见Stephan T. Lavavej的GN2012演讲,大约在第12分钟左右。)因此,make_shared不仅避免了分配,还分配了更少的总内存。 - KnowItAllWannabe
1
@HannaKhalil char * p = malloc(sizeof(T) + sizeof(control)); T * t = new(p) T(args...); control * c = new(p + sizeof(T)) control(1, 0);. 即 placement-new - Caleth
显示剩余19条评论

33

除了之前提到的情况之外,还有另一个情况使得这两种可能性不同:如果你需要调用一个非公共构造函数(protected或private),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){}
};

我也遇到了相同的问题,决定使用 new,否则我会使用 make_shared。 这里有一个相关的问题:https://dev59.com/mmsz5IYBdhLWcg3wCDl2。 - jigglypuff
这个问题可以使用“友元”来解决吗? - nickt
@nickt,我怀疑这是不可能的,因为对象可能是由从“make_shared”调用的函数创建的,而它们是实现特定的。 - AntonK

29

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

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


2
我正确理解了你的第一个观点。你能否详细说明一下第二个关于异常安全的观点或提供一些链接? - Anup Buchke

11
我看到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


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

2
make_shared 不适用的第二种情况是当您想要指定自定义删除器时。 - KnowItAllWannabe

4

Shared_ptr:执行两次堆分配

  1. 控制块(引用计数)
  2. 被管理的对象

Make_shared:只执行一次堆分配

  1. 控制块和对象数据。

1
我认为mpark的回答中关于异常安全性的部分仍然是一个有效的问题。当像这样创建shared_ptr时:shared_ptr< T >(new T),new T可能会成功,而shared_ptr的控制块分配可能会失败。在这种情况下,新分配的T将泄漏,因为shared_ptr无法知道它是在原地创建的,并且可以安全地删除它。或者我有什么遗漏吗?我不认为对函数参数评估的更严格规则在这里有任何帮助...

1
标准保证在这种情况下对象将被删除。 - erzya

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

事实是,使用make_shared比使用new花费的时间多一倍。因此,使用new会有两个堆分配,而使用make_shared只有一个。也许这是一个愚蠢的测试,但它难道不表明使用make_shared比使用new需要更多的时间吗?当然,我只是在谈论使用的时间。

6
那个测试有点毫无意义。测试是否在发布配置下进行,并关闭了优化?另外,你的所有项目都立即释放了,所以这并不现实。 - Phil1970
调试性能也很重要。优化会降低调试性能,从而影响开发者的体验。 - Kiruahxh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接