C + + 中 make_share 与标准 share_ptr 的区别

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是如何有效的。我已经给出了一个例子以供参考。

162158 次浏览

区别在于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以来,这不是一个问题,因为函数参数的求值顺序发生了变化。具体来说,函数的每个参数都需要在对其他参数求值之前完全执行。

由于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的缺点

引用凯西的评论:

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

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

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

回到std::make_shared

由于std::make_shared对控制块和托管对象都进行了单一的堆分配,因此没有办法为控制块和托管对象独立释放内存。我们必须等待,直到我们可以释放控制块和托管对象,这恰好是直到没有__abc1或__abc2活的时候。

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

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

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

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

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

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

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

Shared_ptr:执行两个堆分配

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

Make_shared:只执行一次堆分配

  1. 控制块和对象数据。

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

我认为mpark先生的回答中的异常安全部分仍然是一个合理的担忧。当像这样创建一个shared_ptr时:T >(新T),新T可能成功,而shared_ptr分配控制块可能失败。在这种情况下,新分配的T会泄漏,因为shared_ptr无法知道它是就地创建的,删除它是安全的。还是我遗漏了什么?我不认为更严格的函数参数计算规则在这里有任何帮助…