3

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?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Taylor
  • 5,871
  • 2
  • 30
  • 64

2 Answers2

9

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>.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Would be cool to have a version of shared_from_this that assumes the object was allocated using make_shared (and avoids the inheritance). I'd use it! – Taylor Jan 20 '15 at 19:04
  • @Taylor you'd also have to know what type it was constructed as, now that I think about it -- if we have a pointer-to-Q, and Q is located at offset 47 from the start of the object which is actually a W, finding the shared pointer block from a `Q*` is challenging. It might be doable with a virtual inheritance table, but I am uncertain. – Yakk - Adam Nevraumont Jan 20 '15 at 19:13
  • I never use virtual inheritance, so I'd love something (obviously not in std) that just made the common case easy. – Taylor Jan 20 '15 at 19:23
  • Even if you never use `new T` anywhere, you can have `shared_ptr` that were *not* created with `make_shared`, f.ex. those pointing to base subobjects. – curiousguy Sep 07 '19 at 06:46
0

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);
}

Live Demo

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();
}

Live Demo

Taylor
  • 5,871
  • 2
  • 30
  • 64
  • 1
    Because... what? You compared one thing to... `void()`. _"so this implementation would not perform as well"._ I'm happy you had an insight, but if you want to usefully share this insight (and get appreciation for that, no doubt) it's a good idea to _actually_ exposition it. (_Otherwise it's like handing out your ungroomed back-of-napkin notes to a book editor and say "you can publish this"_) – sehe Jan 20 '15 at 20:22
  • Right, because stack overflow is somehow equivalent in formality to publishing a book. LOL! The longer answer above echoes what I said ("a performance hit compared to the current state for people who do need it (the mutex, the lookup, etc)."). The word exposition is a noun, not a verb. I'm interested in information, not points. – Taylor Jan 20 '15 at 21:54
  • 1
    Re: "formality" that's not what I meant. I pointed out that your reasoning here is incomplete. As such it is both not useful (people can not follow your reasoning to gain your insight) and unverifiable. (Thanks for correcting my English) – sehe Jan 20 '15 at 22:24
  • @sehe Ok, I added a demo, so now it is verifiable. – Taylor Jan 20 '15 at 23:10
  • @sehe, also, what is incomplete about my reasoning? How hard is it to understand that accessing a global hash table every time you create or delete an object is going to cause overhead? – Taylor Jan 20 '15 at 23:19
  • @Taylor Well, is your live demo that big you can't provide the code sample inline? – πάντα ῥεῖ Jan 20 '15 at 23:26
  • 1
    @Taylor You read my mind! (I'll abandon the explanation I was trying to cram into a comment. English not being my native language this takes a fair amount of time). The assumption makes this post not self-contained. It's not informative (since obvious) to people who already knew. As given, it appears to be no more than a corollary to the subject post, that somehow got detached from it. – sehe Jan 20 '15 at 23:41
  • The assumption being that the reader is somewhat familiar with how enable_shared_from_this works? – Taylor Jan 20 '15 at 23:51