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

oug3syen  于 2023-01-03  发布在  其他
关注(0)|答案(8)|浏览(198)
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是如何高效的。我已经给出了一个例子作为参考。

xmq68pz9

xmq68pz91#

不同之处在于std::make_shared执行一次堆分配,而调用std::shared_ptr构造函数执行两次。

在哪里进行堆分配?

std::shared_ptr管理两个实体:

  • 控制块(存储诸如引用计数、类型擦除删除器等 meta数据)
  • 被管理对象

std::make_shared执行单个堆分配,考虑控制块和数据所需的空间。在另一种情况下,new Obj("foo")调用托管数据的堆分配,std::shared_ptr构造函数为控制块执行另一个堆分配。
有关详细信息,请查看cppreference上的实施说明

更新I:例外-安全性

注(2019年8月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"));

更新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活动时,我们为控制块释放内存(可能更晚)。

9avjhtql

9avjhtql2#

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

pieyvz9o3#

共享指针管理对象本身以及包含引用计数和其他内务数据的小对象。make_shared可以分配单个内存块来保存这两个对象;从指向已分配对象的指针构造共享指针将需要分配第二块来存储引用计数。
除了这种效率之外,使用make_shared意味着您根本不需要处理new和原始指针,从而提供了更好的异常安全性--在分配对象之后、将其赋给智能指针之前,不可能抛出异常。

ljo96ir5

ljo96ir54#

我发现std::make_shared有一个问题,它不支持private/protected构造函数
std::shared_ptr(new T(args...))如果在可访问的上下文中执行,则可以调用T的非公共构造函数,而std::make_shared需要对所选构造函数的公共访问。
https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared#Notes

xbp102n0

xbp102n05#

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

fcipmucu

fcipmucu6#

Shared_ptr:执行两个堆分配
1.控制块(引用计数)
1.正在管理的对象
Make_shared:仅执行一次堆分配
1.控制块和对象数据。

x3naxklr

x3naxklr7#

我认为mpark先生的回答中的异常安全部分仍然是一个有效的关注点。当创建一个shared_ptr时,如下所示:shared_ptr< T >(new T),新的T可能会成功,而shared_ptr的控制块分配可能会失败。在这种情况下,新分配的T会泄漏,因为shared_ptr无法知道它是就地创建的,删除它是安全的。或者我错过了什么?我不认为函数参数求值的更严格规则在这里有任何帮助...

nimxete2

nimxete28#

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

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

相关问题