Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Destruction of each other in c++ shared_ptr

Tags:

c++

c++11

everyone. I find a problem in c++ deconstruction. I will show you my code below:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class B;

class A{
public:
    typedef shared_ptr<A> Ptr;

    shared_ptr<B> b;

    int index;

    A(const shared_ptr<B>& b_, int index_):b(b_), index(index_){}

    ~A(){cout << "A is deleted." << endl;
         cout << "A id is " << index << endl;}



};

class B{
public:
    typedef shared_ptr<B> Ptr;

    vector<A::Ptr> all_a;

    void addA(){
        for(int i=0; i<10; i++){
            auto a = make_shared<A>(Ptr(this), i);

            all_a.push_back(a);
        }

    }

    int index;

    B(int index_):index(index_){

    }

    ~B(){
        cout << "B is deleted." << endl;
        cout << "B id is " << index << endl;
    }


};



int main(){

    B::Ptr b = make_shared<B>(5);

    b->addA();

    return 0;
}

A part of results is is

... 
B is deleted.
B id is 5
A is deleted.
A id is 8
B is deleted.
B id is 5
A is deleted.
A id is 9
B is deleted.
B id is 5
double free or corruption (out)
[1]    29094 abort (core dumped)  ./a.out

What is it happened with my code, how can i debug this problem. I thought the shared_ptr has some bug in this case.

like image 277
user9885108 Avatar asked Jun 02 '18 12:06

user9885108


2 Answers

There's one problem at play and another that'll pop up when you fix it.

Part 1: shared_ptrs that don't share

So, the reason you see a lot of destructor calls for the same B object and crash with a double-free error is this line:

auto a = make_shared<A>(Ptr(this), i);

In particular, Ptr(this) makes a shared_ptr<B> from this that stands alone -- it cannot know that there are other shared_ptrs somewhere else that feel responsible for the same object, it has its own reference count, and it'll believe that no one else is using the object it points to when that goes to zero. For all intents and purposes, it behaves like a unique_ptr because it never shares its reference count with another shared_ptr (except temporaries).

Unfortunately, you don't know a shared_ptr that manages the object you're working on inside the member function. Fortunately, there's something in the standard library to help with that: std::enable_shared_from_this It works like this:

class B : public std::enable_shared_from_this<B> {

// ...

   auto a = make_shared<A>(shared_from_this(), i);

Note that if you do this, the code expects that B will only ever be manged by shared_ptrs. Although, really, it already does that, so that's okay.

Part 2: Round and round we go -- cyclic reference counts

Now, once you put that into your program, it'll not crash anymore. It'll also not print anything, as if nothing were ever deleted. What gives?

In fact, nothing is ever deleted, and the reason is that as far as all those shared_ptrs are concerned, their objects never go out of use. We've stumbled upon the main reason that reference-counting does not a full garbage collector make.

Your B object knows 10 A objects, but I'll consider only one of them -- it's just the thing I'm about to show 10 times. They have shared_ptrs to each other, and there are however many external shared_ptr that keep them alive as long as they are needed. Once all those external references are gone, you expect them to be deleted, but consider what the situation at that point looks like:

+-----+   shared_ptr<A>   +-----+
|     | ----------------> |     |
|  b  |                   |  a  |
|     | <---------------- |     |
+-----+   shared_ptr<B>   +-----+

b has a shared_ptr to a, so a has a reference count of 1. a has a shared_ptr to b, so b has a reference count of 1. We're caught in a catch-22: a is in use because b is use, and b is in use because a is in use, so neither is ever deleted.

When working with shared_ptrs, this kind of setup absolutely has to be avoided, and that's why weak_ptr exists. I have to guess which way you want the ownership mechanics to go, but for example you could have non-owning references from the A to the B objects like so:

class A{
    // ...
    weak_ptr<B> b;

and in B:

void addA(){
    for(int i=0; i<10; i++){
      auto a = make_shared<A>(weak_from_this(), i);
      all_a.push_back(a);
    }
}

Then the objects in all_a will not keep the B object that owns them alive. To use it, you would write

std::shared_ptr<B> b2 = b.lock();

to have a shared_ptr that keeps it alive while you're working with it.

like image 146
Wintermute Avatar answered Oct 08 '22 03:10

Wintermute


When you do shared_ptr<B>(this) twice, you do not get 2 shared_ptr which manage this successfully. You get 2 shared_ptr with separate control blocks each of which thinks they are the only manager of this . Inevitably this will lead to a double delete or worse.

One way to solve this problem is to use shared_from_this() instead of this; see here for example.

like image 33
M.M Avatar answered Oct 08 '22 05:10

M.M