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

Posted

技术标签:

【中文标题】C++ 中 make_shared 和普通 shared_ptr 的区别【英文标题】:Difference in make_shared and normal shared_ptr in C++ 【发布时间】:2014-01-20 15:36:53 【问题描述】:
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

很多 google 和 *** 帖子都在这上面,但我不明白为什么 make_shared 比直接使用 shared_ptr 更有效。

有人可以逐步解释我创建的对象和两者完成的操作的顺序,以便我能够理解make_shared 的效率如何。上面我举了一个例子供参考。

【问题讨论】:

效率并不高。使用它的原因是为了异常安全。 STL 在他的第 9 频道视频中报道了这一点。可能是this one。 @Yuushi:异常安全是使用它的一个很好的理由,但它也更有效率。 32:15 是他在我上面链接的视频中开始的地方,如果有帮助的话。 次要代码风格优势:使用make_shared你可以写auto p1(std::make_shared&lt;A&gt;())并且p1会有正确的类型。 【参考方案1】:

不同之处在于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++ 允许对子表达式进行任意求值顺序,因此一种可能的顺序是:

    new Lhs("foo")) new Rhs("bar")) std::shared_ptr&lt;Lhs&gt; std::shared_ptr&lt;Rhs&gt;

现在,假设我们在第 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的cmets:

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

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

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

返回std::make_shared

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

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

【讨论】:

提一下make_shared 的小极端情况也是个好主意:因为只有一个分配,所以在控制块不再使用之前不能释放指针的内存。 weak_ptr 可以使控制块无限期地保持活动状态。 另一个更具风格的观点是:如果您始终使用make_sharedmake_unique,您将不会拥有原始指针,并且可以将每次出现的new 视为代码异味。 如果shared_ptr只有一个,没有weak_ptrs,在shared_ptr实例上调用reset()会删除控制块。但这与是否使用 make_shared 无关。使用make_shared 会有所不同,因为它可以延长为托管对象分配的内存 的生命周期。当shared_ptr 计数为 0 时,托管对象的析构函数将被调用,而不管make_shared,但只有在使用make_shared 时才能释放其内存。希望这能让它更清楚。 另外值得一提的是,make_shared 可以利用“我们知道你住在哪里”的优化,该优化允许控制块是一个更小的指针。 (有关详细信息,请参阅大约 12 分钟的 Stephan T. Lavavej's GN2012 presentation。)因此 make_shared 不仅避免了分配,而且还分配了更少的总内存。 @HannaKhalil:这也许是您正在寻找的领域......? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH【参考方案2】:

除了已经提到的之外,还有另一种情况,两种可能性不同:如果您需要调用非公共构造函数(受保护的或私有的),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。这是一个相关的问题:***.com/questions/8147027/….【参考方案3】:

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

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

【讨论】:

我正确理解了您的第一点。关于异常安全的第二点,您能否详细说明或提供一些链接?【参考方案4】:

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

【讨论】:

【参考方案5】:

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

【讨论】:

make_shared 不合适的第二种情况是您要指定自定义删除器。【参考方案6】:

Shared_ptr: 执行两次堆分配

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

Make_shared: 只执行一次堆分配

    控制块和对象数据。

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

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

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 需要更多时间吗?当然,我说的只是使用的时间。

【讨论】:

那个测试有点毫无意义。测试是否在发布配置中进行了优化?此外,您的所有物品都会立即释放,因此不现实。

以上是关于C++ 中 make_shared 和普通 shared_ptr 的区别的主要内容,如果未能解决你的问题,请参考以下文章

C++ shared_ptr make_shared是什么意思

C++ 复制指针所指向的对象 make_shared shard_ptr

在我的测试中,make_shared(boost 或 stl)似乎比 shared_ptr+new 稍慢

linux C++共享指针(std::shared_ptrstd::make_shared)共享对象,reset()重置共享指针,use_count()查看引用计数

linux C++共享指针(std::shared_ptrstd::make_shared)共享对象,reset()重置共享指针,use_count()查看引用计数

C++中的shared_ptr和weak_ptr