c++c++11shared-ptrmake-shared

Was the raw-pointer constructor of shared_ptr a mistake?


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:

  1. the ability to free the object separately from the control block (which is nice if you have long lived weak_ptrs)

  2. compatibility with libraries that hand you raw pointers and the responsibility to free them

  3. the ability to hold arbitrary resources with custom deleters (or no deleter, for non-owning pointers)

  4. 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:

  1. a separate pointer to the control block
  2. (potentially) more complex atomic_compare_exchange logic, may not be worth it.

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).


Solution

  • 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:

    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.