In hindsight, given make_shared
, would shared_ptr
have a constructor that takes a raw pointer had it been introduced with C++11?
Are there strong arguments or use cases in favor of this constructor?
It would have avoided the well documented pitfall of exception-safety and the memory allocation/performance advantage of using make_shared
.
I believe another advantage of requiring shared_ptr
construction via make_shared
would be that it could be a single pointer under the hood, lowering its memory use and making things like atomic_compare_exchange a lot simpler (and possibly more efficient). (see presentation from C++Now)
I understand that a shared_ptr that basically is an intrusive_ptr (with the object and the control block coalesced) would lack features the current std::shared_ptr has. Like:
the ability to free the object separately from the control block (which is nice if you have long lived weak_ptrs)
compatibility with libraries that hand you raw pointers and the responsibility to free them
the ability to hold arbitrary resources with custom deleters (or no deleter, for non-owning pointers)
the ability to point to a sub-object (e.g., a member) while keeping the parent object alive.
What I'm suggesting is that these features may not be used commonly enough (or in the case of using it as a RAII-wrapper) may not be the best fit, to warrant the extra cost:
In a C++98 world (where shared_ptr was introduced) make_shared is less practical and less user friendly (the lack of perfect forwarding requires reference wrappers and the lack of variadic templates makes the implementation clunky).
The problem with your logic is the belief that the reason why shared_ptr
has a distinction between the managed pointer and the get
pointer is because make_shared
wasn't available. And therefore, if we forced everyone to use make_shared
to create shared_ptr
, we wouldn't need that distinction.
This is incorrect.
You can implement shared_ptr
's pointer-based constructor without that distinction. After all, in the initial creation of a managed shared_ptr
, the get
pointer and the managed pointer are the same. If you wanted shared_ptr
to be the sizeof(T*)
, you could just have the shared_ptr
fetch the get
pointer from the managed block. This is regardless of whether the T
is embedded within the managed block.
So the distinction really has nothing at all to do with make_shared
and its ability to embed the T
within the same memory as the managed block. Or rather, the lack thereof.
No, the distinction between the managed pointer and the get
pointer was created because it added features to shared_ptr
. Important ones. You listed some of them, but you missed others:
The ability to have a shared_ptr
to a base class. That is:
shared_ptr<base> p = make_shared<derived>(...);
To do that, you must have a distinction between what a particular instance points to and what the control block controls.
static_pointer_cast
and dynamic_pointer_cast
(and reinterpret_pointer_cast
in C++17). These all rely on the distinction between the managed pointer and the get
pointer.
enable_shared_from_this
within base classes.A shared_ptr
that points to a member subobject of a type that itself is managed by a shared_ptr
. Again, it requires the managed pointer to not be the same as the get
pointer.
You also seem to trivially dismiss the ability to manage pointers not created by you. That's a critical ability, because it allows you to be compatible with other codebases. Internally, you can use shared_ptr
to manage things made by a library that was written in 1998.
With your way, you divide code into two epochs: pre-C++11, and post-C++11. Your shared_ptr
will do nothing for any code not explicitly written for C++11.
And the thing about wrapping all of these features up into a single type is this:
You don't need another one.
shared_ptr
, because it serves so many needs, can be effectively used almost anywhere. It may not be the absolutely-most-efficient-type-possible, but will do the job in virtually every case. And it isn't exactly slow in doing so.
It handles shared ownership with polymorphism. It handles shared ownership of member objects. It handles shared ownership of memory you didn't allocate. It handles shared ownership of memory with special allocation/deallocation needs. And so forth.
If you need shared-ownership semantics, and you need it to work, shared_ptr
's got your back every time. With your suggested idea, there would always be limitations, something in your way from getting your work done.
A type that works should be preferred by default over one that doesn't.