If you create a class of the form
struct S : std::enable_shared_from_this<S>
{
/* ... */
};
which derives from std:: of itself (using the curiously recurring template pattern), then this class becomes a candidate for special treatment by shared_ptr: The shared_ method will produce a shared_. Some restrictions apply.
Here’s how it works.
template<typename T>
struct enable_shared_from_this
{
using esft_detector = enable_shared_from_this;
std::weak_ptr<T> weak_this;
std::weak_ptr<T> weak_from_this()
{ return weak_this; }
std::shared_ptr<T> shared_from_this()
{ return weak_this.lock(); }
};
When you derive from enable_, you get a secret weak pointer which the C++ standard calls weak_this. The inherited member function weak_ returns that weak pointer, and the inherited member function shared_ returns a shared version of that weak pointer.
Who initializes this weak pointer?
When the control block is created, the shared_ptr<S> constructor snoops at the object that is being managed by the control block. If it uniquely inherits from std:: and does so publicly, then the constructor stashes a weak pointer to the newly-constructed shared_ptr in weak_this.
That’s the only time it happens. If anything goes wrong, you don’t get your weak_this, and the weak_ and shared_ methods throw a “bad weak reference” exception.
Here are some things that could go wrong:
- The
Sobject was never created as part of ashared_ptr. Maybe it was created as a local variable or as a member of a larger structure. - The
Sobject derives fromstd::, but the base class was not public.enable_ shared_ from_ this - The
Sobject derives fromstd::more than once.enable_ shared_ from_ this
Some time ago, I discussed a way to make sure people use make_shared to make the object, which you can use to reduce the likelihood of the first problem.
The second problem is often an oversight, forgetting that base classes of a class are private by default. (Base classes of a struct are public by default.)
The third problem is a more complex oversight which usually comes about when you build a derivation hierarchy out of multiple pieces, unaware that some of the pieces are already using std::.
Okay, so that’s what it does, but how does it work?
The shared_ptr constructor detects the presence of a unique std:: base class by using the esft_detector that I put in the expository declaration.
template<typename T, typename = void>
struct supports_esft : std::false_type {};
template<typename T>
struct inline bool supports_esft<T,
std::void_t<typename T::esft_detector>>
: std::true_type {};
Our first attempt at detecting std:: support is checking whether our marker type esft_ is available. If there is no std:: in the derivation hierarchy, then the type will be missing outright. If it is present but not public, then the check will fail due to the type being inaccessible.
The code that sets the weak pointer uses this detector helper:
template<typename T, typename D>
struct shared_ptr
{
shared_ptr(T* ptr)
{
... do the usual stuff ...
/* Here comes enable_shared_from_this magic */
if constexpr (supports_esft<T>::value) {
using detector = T::esft_detector;
ptr->detector::weak_this = *this;
}
}
... other constructors and stuff ...
};
If the esft_detector is present, then we use it to tell us which specialization of std:: was used, so that we can set that base class’s weak_this.
We can’t stop here, though, because this results in a compilation error if there are multiple std:: base classes.
struct B : std::enable_shared_from_this<B> {};
struct M1 : B {};
struct M2 : B {};
struct D : M1, M2 {};
auto p = std::make_shared<D>();
error: ambiguous reference to base class at
ptr->detector::weak_this = *this;
^^^^^^^^
To avoid this, we also ensure that the detector is unique.
template<typename T>
struct inline bool supports_esft<T,
std::void_t<typename T::esft_detector>>
: std::is_convertible<T *, typename T::esft_detector *>::type {};
If a pointer to T is convertible to a pointer to the detector, then we know that the detector appears only once among the base classes of T.
One could argue that instead of silently ignoring the cases where std:: was declared but could not be used, the language could have said that such a program is ill-formed and produces a compiler error. But no, the language says that if you break rules 2 or 3, then the std:: is simply ignored, and you are left scratching your head trying to figure out where you went astray.
I suspect part of the problem is that it is explicitly legal to use shared_ptr<T> when T is an incomplete type.
Think there’s a typo here: