We know that shared_ptr can have a cyclic dependecy. For example, if object A has a shared_ptr member that points to object B, and object B has a shared_ptr memeber that points to object A, then both objects will "live" until end of program, since the use_count will never be 0.
Why is it not the case if we switched, in the example above, shared_ptr to unique_ptr? A would still point to B and vice versa, so we can not delete neither of them.
Edit: as requested in the comments, a small (contrived) example:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A{
public:
unique_ptr<B> a_ptr;
};
class B{
public:
unique_ptr<A> b_ptr;
};
using namespace std;
int main()
{
A* a = new A();
B* b = new B();
a->a_ptr.reset(b);
b->b_ptr.reset(a);
return 0;
}
A unique_ptr
does not prevent ownership cycles. There are fewer ways in which such a cycle can come up, but more than none. Whatever resource told you otherwise is mistaken.
What is correct is that it is easier to create an ownership cycle with shared pointers. An object could easily keep a copy of a shared pointer in order to keep a needed resource – created and owned by someone else – alive. Your object doesn't know much about the resource other than it exists. Your object is not aware that the resource needs another resource, so the resource did the same thing, copy a shared pointer. In a dynamic program, needs change, so it might be that at some point one of these indirectly needed resources is given a shared pointer to your object, resulting in a cycle. In a sense, too many chefs (owners) spoiled the soup.
With a unique_ptr
, there is no latching onto someone else's object. Ownership usually has a clear direction, from certain objects to others, leaving no room for a cycle. Usually. But not always. As you noted, it is possible to create an ownership cycle with unique_ptr
.
In the interest of having a less contrived example, let's talk about no one's everyone's favorite subject, taxes money. :) There is a tax concept called "dependent" which basically means that one person can mooch off of be supported by another. In the below code, we model the scenario where one person gets fired and has to rely on someone else, a.k.a. move back in with the parents. (For those who missed the humor: this is intended as a fun exercise, and any resemblance to reality is purely coincidental in your head.)
This code is simplified to focus on the ownership cycle that will be created. There are plenty of ways to add things to make it more "realistic", but my intent is not realism. My goal is to present a plausible setup, then create an ownership cycle with a simple typo.
(The code blocks should be combined into one file, but I'm adding some commentary along the way.)
Start with a type to represent a person. This is the object that will be leaked, so it logs construction and destruction. I even remembered to log the copy constructor, even though that will not come into play.
#include <iostream>
#include <memory>
struct TaxPayer {
// For simplicity, support at most one dependent instead using of a vector.
std::unique_ptr<TaxPayer> dependent;
// Log construction and destruction,
TaxPayer() { std::cout << "TaxPayer()\n"; }
TaxPayer(const TaxPayer&) { std::cout << "TaxPayer(copy)\n"; }
~TaxPayer() { std::cout << "~TaxPayer()\n"; }
};
Next, we model a "household", which for our purposes is more of a place to send bills than anything related to "family". Often a dependent moves out and establishes a new "household", but we need to support the reverse direction as well. So this type provides a method for abandoning the household and becoming a dependent.
struct Household {
// For simplicity, support at most one income per household.
std::unique_ptr<TaxPayer> bread_winner;
// For convenience, default to having someone in the household.
Household() : bread_winner(std::make_unique<TaxPayer>()) {}
// Stuff happens. The bread winner becomes unemployed and
// abandons the house to live off someone else.
void becomeDependentOn(TaxPayer& sponsor) {
sponsor.dependent = std::move(bread_winner);
}
};
So far I have avoided undesirable elements like raw pointers (code smell) and contrived indirection. Now, though, I will give in and use variable names that are not particularly descriptive. This allows a simple typo to break things.
int main()
{
Household house1;
Household house2;
// Assume some processing was done, and now it's time
// to move house2 into house1 as a dependent.
house1.becomeDependentOn(*house1.bread_winner);
// Oops! That line was supposed to be:
//house2.becomeDependentOn(*house1.bread_winner);
}
Oh no! I used the wrong variable. When I run this code, I get
TaxPayer()
TaxPayer()
~TaxPayer()
Two objects were created, but only one was destroyed. The object that survived managed to become its own dependent!
The kicker is that changing one character removes the memory leak. It's not stellar coding, but I think it falls under "plausible" rather than "contrived".
That's right, they both still point to each other, except they don't know about it (they don't count the references like shared pointers do).
What will generally happen is, as soon as one of those unique pointers is destroyed, it will delete the pointer. Then, when the other unique pointer is destroyed (goes out of scope or for whatever other reason, that's it's job after all), it will attempt to delete the same pointer again, which invokes undefined behaviour (a crash if things go "well").
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With