I was reading Top 10 dumb mistakes to avoid with C++11 smart pointer. Number #5 reads:
Mistake # 5 : Not assigning an object(raw pointer) to a shared_ptr as soon as it is created !
int main()
{
Aircraft* myAircraft = new Aircraft("F-16");
shared_ptr<aircraft> pAircraft(myAircraft);
...
shared_ptr<aircraft> p2(myAircraft);
// will do a double delete and possibly crash
}
and the recommendation is something like:
Use
make_shared
ornew
and immediately construct the pointer with it.
Ok, no doubt about it the problem and the recommendation.
However I have a question about the design of shared_ptr
.
This is a very easy mistake to make and the whole "safe" design of shared_ptr
could be thrown away by very easy-to-detect missuses.
Now the question is, could this be easily been fixed with an alternative design of shared_ptr
in which the only constructor from raw pointer would be that from a r-value reference?
template<class T>
struct shared_ptr{
shared_ptr(T*&& t){...basically current implementation...}
shared_ptr(T* t) = delete; // this is to...
shared_ptr(T* const& t) = delete; // ... illustrate the point.
shared_ptr(T*& t) = delete;
...
};
In this way shared_ptr
could be only initialized from the result of new
or some factory function.
Is this an underexploitation of the C++ language in the library? or What is the point of having a constructor from raw pointer (l-value) reference if this is going to be most likely a misuse?
Is this a historical accident? (e.g. shared_ptr was proposed before r-value references were introduced, etc) Backwards compatibility?
(Of course one could say std::shared_ptr<type>(std::move(ptr));
that that is easier to catch and also a work around if this is really necessary.)
Am I missing something?
Pointers are very easy to copy. Even if you restrict to r-value reference you can sill easily make copies (like when you pass a pointer as a function parameter) which will invalidate the safety setup. Moreover you will run into problems in templates where you can easily have T* const
or T*&
as a type and you get type mismatches.
So you are proposing to create more restrictions without significant safety gains, which is likely why it was not in the standard to begin with.
The point of make_shared
is to atomize the construction of a shared pointer. Say you have f(shared_ptr<int>(new int(5)), throw_some_exception())
. The order of parameter invokation is not guaranteed by the standard. The compiler is allowed to create a new int, run throw_some_exception
and then construct the shared_ptr which means that you could leak the int (if throw_some_exception
actually throws an exception). make_shared
just creates the object and the shared pointer inside itself, which doesn't allow the compiler to change the order, so it becomes safe.
I do not have any special insight into the design of shared_ptr
, but I think the most likely explanation is that the timelines involved made this impossible:
The shared_ptr
was introduced at the same time as rvalue-references, in C++11. The shared_ptr
already had a working reference implementation in boost
, so it could be expected to be added to standard libraries relatively quickly.
If the constructor for shared_ptr
had only supported construction from rvalue references, it would have been unusable until the compiler had also implemented support for rvalue references.
And at that time, compiler and standards development was much more asynchronous, so it could have taken years until all compiler had implemented support, if at all. (export templates were still fresh on peoples minds in 2011)
Additionally, I assume the standards committee would have felt uncomfortable standardizing an API that did not have a reference implementation, and could not even get one until after the standard was published.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With