There seem to be some edge-cases when using enabled_shared_from_this
. For example:
boost shared_from_this and multiple inheritance
Could shared_from_this
be implemented without using enable_shared_from_this
? If so, could it be made as fast?
There seem to be some edge-cases when using enabled_shared_from_this
. For example:
boost shared_from_this and multiple inheritance
Could shared_from_this
be implemented without using enable_shared_from_this
? If so, could it be made as fast?
A shared_ptr
is 3 things. It is a reference counter, a destroyer and an owned resource.
When you make_shared
, it allocates all 3 at once, then constructs them in that one block.
When you create a shared_ptr<T>
from a T*
, you create the reference counter/destroyer separately, and note that the owned resource is the T*
.
The goal of shared_from_this
is that we can extract a shared_ptr<T>
from a T*
basically (under the assumption it exists).
If all shared pointers where created via make_shared
, this would be easy (unless you want defined behavior on failure), as the layout is easy.
However, not all shared pointers are created that way. Sometimes you can create a shared pointer to an object that was not created by any std
library function, and hence the T*
is unrelated to the shared pointer reference counting and destruction data.
As there is no room in a T*
or what it points to (in general) to find such constructs, we would have to store it externally, which means global state and thread safety overhead and other pain. This would be a burden on people who do not need shared_from_this
, and a performance hit compared to the current state for people who do need it (the mutex, the lookup, etc).
The current design stores a weak_ptr<T>
in the enable_shared_from_this<T>
. This weak_ptr
is initialized whenever make_shared
or shared_ptr<T>
ctor is called. Now we can create a shared_ptr<T>
from the T*
because we have "made room" for it in the class by inheriting from enable_shared_from_this<T>
.
This is again extremely low cost, and handles the simple cases very well. We end up with an overhead of one weak_ptr<T>
over the baseline cost of a T
.
When you have two different shared_from_this
, their weak_ptr<A>
and weak_ptr<B>
members are unrelated, so it is ambiguous where you want to store the resulting smart pointer (probably both?). This ambiguity results in the error you see, as it assumes there is exactly one weak_ptr<?>
member in one unique shared_from_this<?>
and there is actually two.
The linked solution provides a clever way to extend this. It writes enable_shared_from_this_virtual<T>
.
Here instead of storing a weak_ptr<T>
, we store a weak_ptr<Q>
where Q
is a virtual base class of enable_shared_from_this_virtual<T>
, and does so uniquely in a virtual base class. It then non-virtually overrides shared_from_this
and similar methods to provide the same interface as shared_from_this<T>
does using the "member pointer or child type shared_ptr
constructor", where you split the reference count/destroyer component from the owned resource component, in a type-safe way.
The overhead here is greater than the basic shared_from_this
: it has virtual inheritance and forces a virtual destructor, which means the object stores a pointer to a virtual function table, and access to shared_from_this
is slower as it requires a virtual function table dispatch.
The advantage is it "just works". There is now one unique shared_from_this<?>
in the heirarchy, and you can still get type-safe shared pointers to classes T
that inherit from shared_from_this<T>
.
Yes, it could use global hash tables of type
unordered_map< T*, weak_ptr<T> >
to perform the lookup of a shared pointer from this
.
#include <memory>
#include <iostream>
#include <unordered_map>
#include <cassert>
using namespace std;
template<class T>
struct MySharedFromThis {
static unordered_map<T*, weak_ptr<T> > map;
static std::shared_ptr<T> Find(T* p) {
auto iter = map.find(p);
if(iter == map.end())
return nullptr;
auto shared = iter->second.lock();
if(shared == nullptr)
throw bad_weak_ptr();
return shared;
}
};
template<class T>
unordered_map<T*, weak_ptr<T> > MySharedFromThis<T>::map;
template<class T>
struct MyDeleter {
void operator()(T * p) {
std::cout << "deleter called" << std::endl;
auto& map = MySharedFromThis<T>::map;
auto iter = map.find(p);
assert(iter != map.end());
map.erase(iter);
delete p;
}
};
template<class T>
shared_ptr<T> MyMakeShared() {
auto p = shared_ptr<T>(new T, MyDeleter<T>());
MySharedFromThis<T>::map[p.get()] = p;
return p;
}
struct Test {
shared_ptr<Test> GetShared() { return MySharedFromThis<Test>::Find(this); }
};
int main() {
auto p = MyMakeShared<Test>();
assert(p);
assert(p->GetShared() == p);
}
However, the map has to be updated whenever a shared_ptr is constructed from a T*, and before the deleter is called, costing time. Also, to be thread safe, a mutex would have to guard access to the map, serializing allocations of the same type between threads. So this implementation would not perform as well as enable_shared_from_this
.
Update:
Improving on this using the same pointer tricks used by make_shared, here is an implementation which should be just as fast as shared_from_this.
template<class T>
struct Holder {
weak_ptr<T> weak;
T value;
};
template<class T>
Holder<T>* GetHolder(T* p) {
// Scary!
return reinterpret_cast< Holder<T>* >(reinterpret_cast<char*>(p) - sizeof(weak_ptr<T>));
}
template<class T>
struct MyDeleter
{
void operator()(T * p)
{
delete GetHolder(p);
}
};
template<class T>
shared_ptr<T> MyMakeShared() {
auto holder = new Holder<T>;
auto p = shared_ptr<T>(&(holder->value), MyDeleter<T>());
holder->weak = p;
return p;
}
template<class T>
shared_ptr<T> MySharedFromThis(T* self) {
return GetHolder(self)->weak.lock();
}