C ++中make_shared和normal shared_ptr的区别
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo"); std::shared_ptr<Object> p2(new Object("foo"));
许多谷歌和stackoverflow的职位上有这个,但我不明白为什么make_shared
比直接使用shared_ptr
更有效率。
有人可以解释我一步一步创build的对象序列和由两者完成的操作,以便我能够了解make_shared
是如何有效的。 上面给出了一个例子供参考。
不同的是, std::make_shared
执行一次堆分配,而调用std::shared_ptr
构造函数执行两次。
堆分配发生在哪里?
std::shared_ptr
pipe理两个实体:
- 控制块(存储诸如ref-counts,types擦除删除器之类的元数据)
- 被pipe理的对象
std::make_shared
为控制块和数据所需的空间执行单个堆分配计算。 在另一种情况下, new Obj("foo")
为托pipe数据调用堆分配, std::shared_ptr
构造函数为控制块执行另一个分配。
有关更多信息,请参阅cppreference上的实现说明 。
更新I:例外 – 安全
由于OP似乎对事物的exception安全方面感到疑惑,我已经更新了我的答案。
考虑这个例子,
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 ++允许对子expression式进行任意次序的评估,所以可能的sorting是:
-
new Lhs("foo"))
-
new Rhs("bar"))
-
std::shared_ptr<Lhs>
-
std::shared_ptr<Rhs>
现在,假设我们得到了第2步抛出的exception(例如,内存不足exception, Rhs
构造函数抛出了一些exception)。 然后,我们失去了在第一步分配的内存,因为没有机会清理它。 这里问题的核心是原始指针不立即传递给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
缺点
引用凯西的评论:
由于那里只有一个分配,所以指针的内存不能被释放,直到控制块不再被使用。
weak_ptr
可以保持控制块无限期地活着。
为什么weak_ptr
的实例使控制块保持活动状态?
weak_ptr
必须有一个方法来确定pipe理对象是否仍然有效(例如,用于lock
)。 他们通过检查拥有存储在控制块中的被pipe理对象的shared_ptr
的数量来执行此操作。 结果是控制块是活着的,直到shared_ptr
计数和weak_ptr
计数都达到0。
回到std::make_shared
由于std::make_shared
为控制块和pipe理对象分配一个堆,因此无法独立释放控制块和pipe理对象的内存。 我们必须等到我们可以释放控制块和被pipe理对象,直到没有shared_ptr
或者weak_ptr
活着。
假设我们通过new
和shared_ptr
构造函数为控制块和pipe理对象执行了两次堆分配。 然后,当没有shared_ptr
存在时,我们为被pipe理对象(也许更早)释放内存,并在没有weak_ptr
存在的情况下释放控制块的内存(也许以后)。
共享指针pipe理对象本身和包含引用计数和其他pipe家数据的小对象。 make_shared
可以分配一块内存来保存这两个内存; 从指针到已经分配的对象构造一个共享指针将需要分配第二个块来存储引用计数。
除了这个效率之外,使用make_shared
意味着你根本不需要处理new
和原始的指针,从而提供更好的exception安全性 – 在分配对象之后但在将其分配给智能指针之前,不可能抛出exception。
除了已经提到的两种可能性之外,还有另一种情况:如果你需要调用一个非公有的构造函数(protected或private),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){} };
如果你需要在由shared_ptr控制的对象上进行特殊的内存alignment,你不能依赖make_shared,但是我认为这是唯一不使用它的好理由。
关于效率和花费在分配上的时间,我在下面做了这个简单的testing,我通过这两种方法创build了许多实例(一次一个):
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花了两倍的时间比使用新的。 所以,使用新的有两个堆分配,而不是一个使用make_shared。 也许这是一个愚蠢的testing,但是这并不表明使用make_shared需要更多的时间比使用新的? 当然,我只是在谈论时间而已。
Shared_ptr
:执行两个堆分配
- 控制块(引用计数)
- 对象被pipe理
Make_shared
:只执行一个堆分配
- 控制块和对象数据。