This is the last of several posts I wrote related to smart pointers:
In modern C++ applications (C++11 and later), you can replace almost all your naked pointers to shared_ptr
and unique_ptr
in order to have automatic resource administration in a deterministic way so you will not need (almost, again) to release the memory manually.
The “almost” means that there is one scenario where the smart pointers, specifically, the shared_ptr
instances, will not work: When you have circular references. In this scenario, since every shared_ptr is pointing to the other one, the memory will never be released.
Let’s look this scenario in code:
struct Child; struct Parent { shared_ptr<Child> child; ~Parent() { cout << "Bye Parent" << endl; } void hi() const { cout << "Hello" << endl; } }; struct Child { shared_ptr<Parent> parent; ~Child() { cout << "Bye Child" << endl; } }; int main() { auto parent = make_shared<Parent>(); auto child = make_shared<Child>(); parent->child = child; child->parent = parent; child->parent->hi(); }
In this program, the Parent and the Child have pointers to their respective child and
parent (creating a circular reference between both); when you execute the program, you will notice the destructors are never invoked because I used shared_ptr instances.
If we model this problem, probably we can establish the Parent as “owner” of the Child object (it would mean that the child lifetime will be based on the parent lifetime) and the Child will just refer to its parent. If we diagram this using a class diagram, we can draw it like this:
If we use C++11 smart pointers to implement this kind of scenarios, we could use shared_ptr instances to implement the composition relationship and weak_ptr instances to implement the aggregation relationship:
struct Child; struct Parent { shared_ptr<Child> child; ~Parent() { cout << "Bye Parent" << endl; } void hi() const { cout << "Hello" << endl; } }; struct Child { weak_ptr<Parent> parent; ~Child() { cout << "Bye Child" << endl; } }; int main() { auto parent = make_shared<Parent>(); auto child = make_shared<Child>(); parent->child = child; child->parent = parent; child->parent.lock()->hi(); }
When you execute this program, you will notice the destructors are invoked correctly.
weak_ptr
is a wrapper for a shared_ptr
that does not represent ownership of the pointee object and, therefore, does not avoid the object to be released when the parent shared_ptr
reference counter goes to 0.
In my code I am using a weak_ptr
method called lock
. weak_ptr
does not implement the operator->
, so, if you want to access the methods of the pointee object, you need to invoke the lock(9
method. It creates a temporary shared_ptr
instance that, since it increases the pointee reference counter, actually locks the object to be released while it is executing the invoked method.
Other nice feature of weak_ptr
is that it can tell you if its pointee object has been already released. Look at this code:
struct A { int x; A(int x) : x(x) { cout << "HI" << endl; } ~A() { cout << "Bye" << endl; } }; weak_ptr<A> m() { auto a = make_shared<A>(12); cout << a->x << endl; return a; } int main() { auto a = m(); cout << "After m()" << endl; if (a.expired()) cout << "Expired" << endl; else cout << a.lock()->x << endl; }
The function main()
receives an instance of a weak_ptr
. If you look into the code, the function m()
has a shared_ptr
instance, but the object pointed by it lives only inside the function and it is released when returning the weak_ptr
. Back to the main()
function, before accessing the value of x
, I am invoking the method expired()
that returns true if the weak_ptr
instance points to an already released method.
Thus, with unique_ptr
, shared_ptr
, and weak_ptr
, all the scenarios where you need to use dynamically allocated memory are covered and you will not need to release your objects manually anymore.
In your `final` Parent-Child model, you create Child as a shared_ptr in Parent. But Parent owns Child. That makes the model seem a bit odd, doesn’t it?
therefore, does not avoid the object to be released when the parent shared_ptr reference counter goes to 0. Is this is a correct prase?