Smart Pointers in C++ - Part 5
Introduction
Welcome to Part #5 of Smart Pointers in C++!
The Parts #1, #2, #3 and #4 are a prerquisite for Part #3 and can be found here
We have discussed unique_ptr
and shared_ptr
in detail. Let's focus on weak_ptr
in this part.
A weak_ptr
is a bit similar to a shared_ptr
, in that, we can have multiple weak_ptr
s owning a common object. It differs from shared_ptr
by not taking part in the reference count. In other words, a weak_ptr
does not increase reference count of the owned object. A weak_ptr
must be used in conjunction with a shared_ptr
. It's used when we want to observe a resource, but do not want to control the life cycle of the resource.
shared_ptr
A pitfall of One problem with using shared_ptr
is that of Circular references.
Circular reference is a series of references such that each object i
references the object i+1
and the last object references the first, thus forming a reference loop. To find out what's wrong with a circular reference, let's consider an example.
// forward declaration
struct Bar;
struct Foo {
std::shared_ptr<Bar> pBar;
};
struct Bar {
std::shared_ptr<Foo> pFoo;
};
We have two struct
s referencing each other. This is an example of circular reference.
void someFunction() {
// create struct objects
std::shared_ptr<Foo> f = std::make_shared<Foo>();
std::shared_ptr<Bar> b = std::make_shared<Bar>();
// circular reference
f->pBar = b;
b->pFoo = f;
// do something else
}
When the function returns, the two objects created (one each of Foo
and Bar
) go out of scope. According to working of smart pointers, both objects should get automatically destroyed. But, this does not happen.
When destructor for Bar
runs, it checks whether there are any shared references to this object. Currently, there is one ā reference pBar
that belongs to Foo
(b
has already gone out of scope). So the Bar
object initially co-owned by b
will not be destructed. When destructor for Foo
runs, the same happens. It checks for shared references and finds that pFoo
that belongs to Bar
owns the object. So the Foo
object initially co-owned by f
will not be destructed.
When the program ends, there are two objects that never got destroyed. We have a memory leak!
Solution
Make use of weak_ptr
to break the circular reference. So we change the shared_ptr
in the previous example to weak_ptr
.
// forward declaration
struct Bar;
struct Foo {
std::weak_ptr<Bar> pBar;
};
struct Bar {
std::weak_ptr<Foo> pFoo;
};
The code for someFunction()
stays the same.
void someFunction() {
// create struct objects
std::shared_ptr<Foo> f = std::make_shared<Foo>();
std::shared_ptr<Bar> b = std::make_shared<Bar>();
// no circular reference here
f->pBar = b;
b->pFoo = f;
// the copy assignment of weak_ptr is getting called here.
// It automatically *casts* shared_ptr to weak_ptr.
// do something else
}
Now, when the someFunction()
function runs, we will not have a memory leak. Why? Because, when destroying the two objects that were created, there were no shared_ptr
references to those objects. There were weak_ptr
references; and weak_ptr
references do not keep a resource alive. So both the objects get destroyed when their destructor runs.
weak_ptr
to shared_ptr
Converting Using a weak_ptr
we cannot access the owned object. There are no *
and ->
operators defined for weak_ptr
. We have to get a shared_ptr
to the owned object before using it. The lock()
function of weak_ptr
helps us do exactly that. It returns a shared_ptr
object with the information preserved bby the weak_ptr
. If the object has already been destroyed, lock()
returns a shared_ptr
object with default values.
void weakPtrDemo() {
std::shared_ptr<int> sp1, sp2;
std::weak_ptr<int> wp;
sp1 = std::make_shared<int> (20);
wp = sp1; // wp also owns the int
// try getting shared_ptr from wp
sp2 = wp.lock();
sp1.reset(); // sp1 releases the ownership, but sp2 still owns the int
sp1 = wp.lock(); // get the ownership back, now sp1 and sp2 co-own the int
std::cout << "*sp1: " << *sp1 << std::endl; // 20
std::cout << "*sp2: " << *sp2 << std::endl; // 20
sp1.reset();
sp2.reset();
// the int will be freed as no shared pointer owns it.
// lock() returns shared_ptr with default value
// (nullptr in this case)
sp1 = wp.lock();
std::cout << "is sp1 valid: " << std::boolalpha
<< static_cast<bool> (sp1) << std::endl; // false
}
Conclusion
Now that we know how to use smart pointers, let's try and avoid raw pointers and start making use of smart pointers.
Thank you to share your opinion about one of the most popular programming languages. However, since Iām new to it, I often encounter various challenges when working on programming assignment. Fortunately, this resource aids me in overcoming these difficulties. Thanks to it, I can prepare thoroughly and complete my tasks on time.