Reputation: 59
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.
Upvotes: 5
Views: 2097
Reputation: 6881
This is clearly noted in the C++ reference:
The ownership of an object can only be shared with another shared_ptr by copy constructing or copy assigning its value to another shared_ptr. Constructing a new shared_ptr using the raw underlying pointer owned by another shared_ptr leads to undefined behavior.
I remember the old saying: if everything fails read the instruction first.
You also cannot mix plain pointer and shared_ptr in your code. You may be holding on a plain pointer that has already been freed by the shared_ptr using the plain pointe.
Upvotes: 0
Reputation: 44023
There's one problem at play and another that'll pop up when you fix it.
shared_ptr
s that don't shareSo, 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_ptr
s 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_ptr
s. Although, really, it already does that, so that's okay.
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_ptr
s 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_ptr
s 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_ptr
s, 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.
Upvotes: 8
Reputation: 141554
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.
Upvotes: 1