Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I use placement new to reset an object within a shared_ptr?

Let's say I have a class.

class BigData {...};
typedef boost::shared_ptr<BigData> BigDataPtr; 

Then I do:

BigDataPtr bigDataPtr(new BigData());

Later on after I am done with my object and I am sure there no other users for the object.

Is it safe to do the following:

bigDataPtr->~BigDataPtr();
new (&*bigDataPtr) BigData;

Would this let me reset the object without any additional allocations?

like image 275
Nathan Doromal Avatar asked Apr 03 '13 20:04

Nathan Doromal


People also ask

Why would you choose shared_ptr instead of unique_ptr?

Use unique_ptr when you want to have single ownership(Exclusive) of the resource. Only one unique_ptr can point to one resource. Since there can be one unique_ptr for single resource its not possible to copy one unique_ptr to another. A shared_ptr is a container for raw pointers.

What happens when shared_ptr goes out of scope?

All the instances point to the same object, and share access to one "control block" that increments and decrements the reference count whenever a new shared_ptr is added, goes out of scope, or is reset. When the reference count reaches zero, the control block deletes the memory resource and itself.

What is the purpose of the shared_ptr <> template?

std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer. Several shared_ptr objects may own the same object.

Can you return a shared_ptr?

So the best way to return a shared_ptr is to simply return by value: shared_ptr<T> Foo() { return shared_ptr<T>(/* acquire something */); }; This is a dead-obvious RVO opportunity for modern C++ compilers. I know for a fact that Visual C++ compilers implement RVO even when all optimizations are turned off.


3 Answers

There are a few ways to go about this. You can use placement new, and this is guaranteed to be safe for two reasons:

  1. You have already allocated the memory for the object, so you know it’s sized and aligned correctly.

  2. shared_ptr is non-invasive; its sole responsibility is to count references and call the deleter when necessary.

However, consider what can happen if reconstruction of the object fails—i.e., throws an exception:

bigDataPtr->~BigDataPtr();
new (bigDataPtr.get()) BigData;

Then you have a problem: the deleter can be called on a non-constructed object, leading almost certainly to undefined behaviour. I say “almost” because the deleter could be a no-op, in which case all would be well.

Safer, I think, would be to move a new value into the existing object:

*bigDataPtr = BigData(42);

Or add a reset() member function to BigData:

bigDataPtr->reset(42);

Then it’s explicit what your real intent is, and you don’t need to be as concerned about object lifetimes.

like image 50
Jon Purdy Avatar answered Sep 27 '22 20:09

Jon Purdy


Yes it is normally safe. (Nod to Maxim Yegorushkin's observation about a throwing edge case)

Note the typo message below

Boost defines the dereference and -> operators as

template<class T>
typename boost::detail::sp_dereference< T >::type boost::shared_ptr< T >::operator* () const;

template<class T>
typename boost::detail::sp_member_access< T >::type boost::shared_ptr< T >::operator-> () const;

When those detail bits are resolved, you have this

template<class T>
T & boost::shared_ptr< T >::operator* () const

template<class T>
T * boost::shared_ptr< T >::operator-> () const 

So you are dealing with the pointed-to object directly. There are no proxies or other constructs that may interfere with what you're attempting.

As the pointed-to data is concerned, your code:

bigDataPtr->~BigDataPtr();
new (&*bigDataPtr) BigData;

May have a typo. But if you intended:

bigDataPtr->~BigData();
new (&*bigDataPtr) BigData;

It will resolve to

(BigData pointer)->~BigData();
new (&(BigData reference)) BigData;

This is legal, and you are correct that it would avoid the additional allocation normally incurred with an assignment.

like image 34
Drew Dormann Avatar answered Sep 27 '22 20:09

Drew Dormann


It is safe if BigData constructor and destructor do not throw exceptions and bigDataPtr is not shared between threads and no pointers or references exist to dynamically allocated members of BigData (if any).

If the destructor throws an exception you may end up with a partially destroyed object (throwing destructors are not generally recommended and standard containers require that destructors of elements do not throw).

If the constructor throws you may end up destroying the object but not constructing a new one.

If bigDataPtr is shared between threads that may also lead to a race condition unless a locking discipline is used.

If code elsewhere takes references or pointers to dynamically allocated members of BigData, when it creates a new BigData its dynamically allocated members may be allocated at other addresses, so existing pointers and references to the members become invalid.

If you are concerned with dubious dereference in new (&*bigDataPtr) BigData; statement use a plain pointer instead:

BigData* p = bigDataPtr.get();
p->~BigData();
new (p) BigData;
like image 45
Maxim Egorushkin Avatar answered Sep 27 '22 20:09

Maxim Egorushkin