Search code examples
c++c++17shared-ptrweak-ptrenable-shared-from-this

Why does std::enable_shared_from_this allow multiple std::shared_ptr instances?


There are several questions that cover the behavior of std::enable_shared_from_this, but I don't think that this is a duplicate.

Classes that inherit from std::enable_shared_from_this carry a std::weak_ptr member. When the application creates a std::shared_ptr pointing to a subclass of std::enable_shared_from_this, the std::shared_ptr constructor checks the std::weak_ptr, and if it is not initialized, initializes it and uses the std::weak_ptr control block for the std::shared_ptr. However, if the std::weak_ptr is already initialized, the constructor just creates a new std::shared_ptr with a new control block. This sets the application up to crash when the reference count of one of the two std::shared_ptr instances goes to zero and deletes the underlying object.

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);

// Okay, p1 and p2 both have ref count = 2
std::shared_ptr<C> p2 = p->shared_from_this();

// Bad: p3 has ref count 1, and C will be deleted twice
std::shared_ptr<C> p3(p);

My question is: why does the library behave this way? If the std::shared_ptr constructor knows that the object is a std::enable_shared_from_this subclass and bothers to check the std::weak_ptr field, why doesn't it always use same control block for the new std::shared_ptr, thus avoiding a potential crash?

And for that matter, why does the method shared_from_this fail when the std::weak_ptr member is not initialized, instead of just initializing it and returning a std::shared_ptr?

It seems strange that the library works the way that it does, since it fails in situations when it could easily succeed. I'm wondering if there were design considerations/limitations that I don't understand.

I am using Clang 8.0.0 in C++17 mode.


Solution

  • If I understand your question correctly, you would assume that calling the constructor shared_ptr a second time would logically reuse the control block stored in shared_from_this.

    From your point of view, this looks logical. Let's assume for a moment that C is part of a library your are maintaining and the usage of C is part of a user of your library.

    struct C : std::enable_shared_from_this<C> {};
    
    C *p = new C();
    std::shared_ptr<C> p1(p);
    std::shared_ptr<C> p3(p); // Valid given your assumption
    

    Now, you found a way to no longer need the enable_shared_from_this and in the next version of your library, this gets updated to:

    struct C {};
    
    C *p = new C();
    std::shared_ptr<C> p1(p);
    std::shared_ptr<C> p3(p); // Now a bug
    

    Suddenly, perfectly valid code becomes invalid without any compiler error/warning, because of upgrading your library. Where possible this should be prevented.

    At the same time, it will cause a lot of confusion. Cause depending on the implementation of the class you put in shared_ptr, it's either defined or undefined behavior. It's less confusing to make it undefined every single time.

    enable_shared_from_this is a standard workaround for getting a hold of a shared_ptr if you don't have a shared_ptr. A classic example:

     struct C : std::enable_shared_from_this<C>
     {
         auto func()
         {
             return std::thread{[c = this->shared_from_this()]{ /*Do something*/ }};
         }
    
         NonCopyable nc;
     };
    

    Adding your mentioned extra functionality does add extra code whenever you don't need it, just for the check. Not that it would matter that much, though, zero overhead abstractions ain't nearly zero overhead abstractions.