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?
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();
}
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 aT*
, you create the reference counter/destroyer separately, and note that the owned resource is theT*
.The goal of
shared_from_this
is that we can extract ashared_ptr<T>
from aT*
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 theT*
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 needshared_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 theenable_shared_from_this<T>
. Thisweak_ptr
is initialized whenevermake_shared
orshared_ptr<T>
ctor is called. Now we can create ashared_ptr<T>
from theT*
because we have "made room" for it in the class by inheriting fromenable_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 aT
.When you have two different
shared_from_this
, theirweak_ptr<A>
andweak_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 oneweak_ptr<?>
member in one uniqueshared_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 aweak_ptr<Q>
whereQ
is a virtual base class ofenable_shared_from_this_virtual<T>
, and does so uniquely in a virtual base class. It then non-virtually overridesshared_from_this
and similar methods to provide the same interface asshared_from_this<T>
does using the "member pointer or child typeshared_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 toshared_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 classesT
that inherit fromshared_from_this<T>
.