Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected behavior from shared_from_this when creating multiple shared_ptr "families" over same object

Here is some example code (online here):

#include <memory>

struct Foo : public std::enable_shared_from_this<Foo> {};

void example()
{
    auto sharedFoo = std::make_shared<Foo>();
    std::shared_ptr<Foo> nonDeletingSharedFoo(sharedFoo.get(), [](void*){});
    nonDeletingSharedFoo.reset();
    sharedFoo->shared_from_this(); // throws std::bad_weak_ptr
}

What I see (under multiple compilers) is that when nonDeletingSharedFoo is reset, the weak_ptr used internally by enable_shared_from_this expires, so the subsequent call to shared_from_this fails.

I expected nonDeletingSharedFoo to have a completely separate ref count from sharedFoo since it was constructed from a raw pointer, but obviously it is still affecting the weak count of the Foo object's internal weak_ptr. I assume this is because the shared_ptr constructor and/or destructor do something special when the pointed-to type implements enable_shared_from_this.

So does this code violate the standard? Is there a solution, or is it just not possible to have multiple shared_ptr "families" over an object that implements enable_shared_from_this?

like image 510
dlf Avatar asked Mar 25 '15 15:03

dlf


1 Answers

You're in a gray area here: enable_shared_from_this is typically implemented by having shared_ptr constructors that take ownership of a raw pointer to an object derived from enable_shared_from_this set a weak_ptr contained inside the object. Thus later calls to shared_from_this() have something to return. When you "reparent" sharedFoo the original weak_ptr value is being overwritten so that it contains an expired value when you finally call shared_from_this.

It's possible that this behavior is forbidden by the standard, but I think it's more likely the intent that it is allowed and that the semantics of ownership are a bit underspecified in this admittedly niche corner case. The standard does note that "The shared_ptr constructors that create unique pointers can detect the presence of an enable_shared_from_this base and assign the newly created shared_ptr to its __weak_this member." ([util.smartptr.enab]/11). Despite the fact that notes are non-normative, I think it speaks to the intent of the standard.

You can avoid the problem altogether by creating a truly empty shared_ptr that doesn't share ownership but nonetheless points at sharedFoo:

std::shared_ptr<Foo> nonDeletingSharedFoo(std::shared_ptr<Foo>(), sharedFoo.get());

This takes advantage of the "aliasing" constructor:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T* p) noexcept;

which creates a shared_ptr that shares ownership with r, in this case an empty default-constructed shared_ptr, and points at p. The result is an empty (non-owning) shared_ptr that points at the same object as sharedFoo.

You are responsible for ensuring that such a non-owning pointer is never dereferenced after the lifetime of the referent ends. It would probably be better to clean up the design so that you either truly share ownership, or use a raw pointer instead of the "non-owning shared_ptr" hack.

like image 73
Casey Avatar answered Oct 24 '22 23:10

Casey