3

I was using shared_ptr in a project. And at one point I had to store the raw pointers as a void then later convert it back to its shared_ptr form in a callback where the void* was passed. But for some reason the code kept crashing. I didn't understand why since I wasn't getting any compiler errors or warnings. But I noticed that when I was inheriting from std::enable_shared_from_this I wasn't assigning it to be a public inheritance. And that's what was causing the crash.

I wrote an example code and I'm just wondering why it happens.

#include <memory>
#include <iostream>

class TestShared : public std::enable_shared_from_this<TestShared>{
private:
    int32_t id;
public:
    TestShared(int32_t id){
        this->id = id;
    }
    std::shared_ptr<TestShared> getshared(){
        return shared_from_this();
    }
    int32_t getid(){
        return id;
    }
};

int main(){
    std::shared_ptr<TestShared> ts(new TestShared(0xFF));
    void* tsp = ts.get();
    std::shared_ptr<TestShared> tsn = ((TestShared*)tsp)->getshared();
    std::cout << std::hex << tsn->getid();
    return 0;
}

So that code will execute and run fine and I get the expected result.

But when I remove public from the inheritance:

#include <memory>
#include <iostream>

class TestShared : std::enable_shared_from_this<TestShared>{
private:
    int32_t id;
public:
    TestShared(int32_t id){
        this->id = id;
    }
    std::shared_ptr<TestShared> getshared(){
        return shared_from_this();
    }
    int32_t getid(){
        return id;
    }
};

int main(){
    std::shared_ptr<TestShared> ts(new TestShared(0xFF));
    void* tsp = ts.get();
    std::shared_ptr<TestShared> tsn = ((TestShared*)tsp)->getshared();
    std::cout << std::hex << tsn->getid();
    return 0;
}

Then it results in a crash. So why does public make a difference here and why doesn't the compiler give a warning/error?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Irelia
  • 3,407
  • 2
  • 10
  • 31
  • Unrelated: why do you need to cast the owned pointer to `void*`? It looks very out of place alongside `shared_ptr`. – alter_igel Jun 10 '19 at 17:43
  • Because I was a using a C library which triggered a callback I had implemented. The object passed into the callback had a user contenxt which was a void*. So I casted my objects to void* as a context to be passed in the callback. The void* in this specific example was just to replicate exactly what I was doing in my project. – Irelia Jun 10 '19 at 18:41
  • that makes total sense; carry on. I was just concerned you might be doing some sort of type punning or polymorphism hack but it seems you know what you're doing :-) – alter_igel Jun 10 '19 at 19:10

2 Answers2

5

Being public is important because the shared_ptr system needs to access the enable_shared_from_this base class of the given type. And it can't do that if it's not publicly accessible from the given type.

There is no warning/error for a non-accessible base class because there is no way for the system to know that your code is wrong.

It is conceptually OK to use a shared_ptr constructor that can "enable shared_from_this" even if enable_shared_from_this is private. Why? Consider the following:

class B : public enable_shared_from_this<B> {...};

class D : private B {...};

Now, B expects to be able to do shared_from_this gymnastics. But D privately inherited from it. So D's relationship to B (and thus to B::shared_from_this) is private. Maybe D is using B in such a way that it doesn't trigger any of the shared_from_this usage.

So if D is not relying on B::shared_from_this, if B is just an implementation-detail of D, why is it an error if someone puts a D in a shared_ptr?

There is no test you can come up with which will not give rise to false positives like this. Therefore, if the enable_shared_from_this base class is not accessible, then shared_ptr constructors that could try to use it simply do not attempt to use it.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Ah okay so it's a matter of inheritance and semantics itself. Not necessarily to do with shared_ptr? – Irelia Jun 10 '19 at 16:19
  • @Nina yes, that's right. `private` generally has the semantics of "implementation detail; look away and do not touch." So it makes sense that a class cannot be queried for what it privately inherits from. – alter_igel Jun 10 '19 at 19:08
  • I think that, if you `enable_shared_from_this`, it's because you want to get a shared pointer to `this` from a class method. I don't know that it's common to have a class that can handle being in both shared and non-shared pointers. I think I would rather have the false positives. – wreckgar23 Feb 14 '21 at 16:52
  • > `shared_ptr` system needs to access the `enable_shared_from_this` base class of the given type. How is this implemented? – B_Dex_Float Jun 01 '21 at 15:16
  • 1
    @B_Dex_Float: The obvious way: `shared_ptr` is a friend of `enable_shared_from_this`, so it can access its internals and play around as needed. This is why your type needs to make `enable_shared_from_this` a `public` base class: so that it can convert a pointer to your type into a pointer to the `enable_shared_from_this` base class. – Nicol Bolas Jun 01 '21 at 15:33
  • Ahh so private inheritance it's not possible for "outsiders" to convert derived* to base*, got it. Thanks for your time! [Link for reference](https://isocpp.org/wiki/faq/private-inheritance#priv-inherit-like-compos) – B_Dex_Float Jun 01 '21 at 15:47
3

According to https://en.cppreference.com/w/cpp/memory/enable_shared_from_this

A common implementation for enable_shared_from_this is to hold a weak reference (such as std::weak_ptr) to this. The constructors of std::shared_ptr detect the presence of an unambiguous and accessible (since C++17) enable_shared_from_this base and assign the newly created std::shared_ptr to the internally stored weak reference if not already owned by a live std::shared_ptr (since C++17).

If the inheritance is public, then when ts is initialized, it "records" in the enable_shared_from_this base subobject that it is the owner of the TestShared object. When getshared is later called, the base subobject is consulted, and a new shared_ptr object is created that shares ownership with ts.

If the inheritance is not public, then when ts is initialized, it doesn't "know" that there is an enable_shared_from_this subobject that it needs to write to. Thus, when getshared is called, the enable_shared_from_this subobject doesn't contain any information about who currently owns the object. In C++17, this results in an exception; before C++17, the result is undefined.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • So if I had a class that allowed inheritance from it, how would my class know if the child class is doing a public inheritance or a private inheritance? I'm guessing that enable_shared_from_this only allows shared_from_this() to be called under public, but not private? – Irelia Jun 10 '19 at 16:21
  • @Nina I'm sorry, I didn't understand this: "How would my class if the child class is doing a public inheritance or a private inheritance" – Brian Bi Jun 10 '19 at 16:22
  • sorry, I made a typo in the comment. "How would my class Know". – Irelia Jun 10 '19 at 16:23
  • 1
    @Nina a class has no way of knowing what kind of access a derived class specified when inheriting it, and should not need to know. If someone chooses to use private inheritance, they are the ones responsible for not using `enable_shared_from_this`. – Brian Bi Jun 10 '19 at 16:25