The C++ Programming Language
Part 13: C++ Smart Pointers or Modern Memory Management
So far we have covered all the key features of C++. In Part 4 we saw how to handle memory, request and allocate memory according to the needs of our program and algorithms. This is a very strong attribute of the language giving great flexibility to handle data. The only drawback is that it requires a lot of work and attention from our side to keep our code safe and stable.
C++ Standard Library offers Smart Pointers to help us with memory management, automate deallocation of memory and avoid any errors caused by bad pointer usage. This help is Smart Pointers.
Why Smart Pointers
In Raw memory management we have a code like this:
#include <iostream> // Standard Library header
class Sample
{
int id;
public:
Sample(int _id = 0) : id(_id) {
std::cout << "Sample Constructor: " << id << "\n";
}
~Sample() {
std::cout << "Sample Destructor: " << id << "\n";
}
void show() {
std::cout << "Sample ID: " << id << "\n";
}
};
int main()
{
std::cout << "Hello from part 13!\n";
// old fashioned manual memory management
std::cout << "------------------ old fashioned manual memory management\n";
{
// when we do not use smart pointers
// we have to manually manage memory
Sample* ptr1 = new Sample(1); // Raw pointer
//delete ptr1; // Manually delete to avoid memory leak
}
return 0;
}
Here we have to manually delete the allocated object otherwise we will have a memory leak.
Smart Pointers on the other hand utilize RAII (Resource Acquisition Is Initialization aka Constructor and Destructor), to automate memory allocation and deallocation and pointer access. Here is a simple implementation of Smart Pointers:
// naive smart pointer implementation
template<typename T>
class smart_pointer{
T* ptr;
public:
smart_pointer(size_t size){
std::cout << "smart_pointer Constructor\n";
ptr = new T[size]; // allocate array of T
}
~smart_pointer() {
std::cout << "smart_pointer Destructor\n";
delete[] ptr; // deallocate array
}
// access the raw pointer via dereference operator
T* operator->() {
return ptr; // return reference to element at index
}
};
Here is a sample use of this pointer:
// using naive smart pointer
std::cout << "------------------ using naive smart pointer\n";
{
smart_pointer<Sample> ptrNaive(2); // Using naive smart pointer to create an object
ptrNaive->show();
}
Although simple, this example perfectly illustrates the idea behind Smart Pointers.
Smart Pointers in the C++ Standard Library
To access smart pointers in C++ we need the
std::unique_ptr
A std::unique_ptr is a smart pointer that manages the lifetime of a dynamically allocated object. It has exclusive ownership of the object, which means that only one unique_ptr can have ownership of the object. Ownership can only be transferred to another unique_ptr via std::move.
The key characteristics of unique_ptr are:
Automatic cleanup: it deletes the allocated resource when it goes out of scope.
Move semantics: Ownership is transferred with std::move
Safety: Guarantees object deletion.
Flexibility: It can handle both single objects (unique_ptr
Custom deleters: Allows user-defined cleanup code.
Here is a sample code snippet:
#include <iostream> // Standard Library header
#include <memory> // For smart pointers
void someFunction(std::unique_ptr<Sample> ptr)
{
ptr->show();
// ptr will be destroyed when it goes out of scope
}
int main()
{
std::cout << "Hello from part 13!\n";
// using smart pointers
std::cout << "------------------ using smart pointers\n";
{
std::unique_ptr<Sample> ptr2(new Sample(2)); // Using unique_ptr
ptr2->show();
std::unique_ptr<Sample[]> ptr2b(new Sample[2]); // Using unique_ptr for array
ptr2b[0].show(); // Access some element
std::unique_ptr<Sample> ptr3;
ptr3 = std::move(ptr2); // Transfer ownership
if (!ptr2) {
std::cout << "ptr2 is now null after move.\n";
}
ptr3->show();
someFunction(std::move(ptr3)); // Transfer ownership
}
return 0;
}
std::shared_ptr
As the name implies shared_ptr allows multiple smart pointers to share ownership of a resource. It uses reference counting to manage the lifetime of the resource.
Here is a sample that illustrates the use of shared_ptr and the way it keeps reference count:
void someFunction(std::shared_ptr<Sample> ptr)
{
std::cout << "Reference Count inside function: " << ptr.use_count() << "\n";
ptr->show();
}
int main()
{
std::cout << "Hello from part 13!\n";
// using shared_ptr
std::cout << "------------------ using shared_ptr\n";
{
std::shared_ptr<Sample> ptr5(new Sample(5)); // Using std::shared_ptr
{
std::shared_ptr<Sample> ptr6 = ptr5; // Shared ownership
std::cout << "Reference Count: " << ptr5.use_count() << "\n";
ptr6->show();
} // ptr6 goes out of scope here
std::cout << "Reference Count after ptr6 goes out of scope: " << ptr5.use_count() << "\n";
someFunction(ptr5); // Pass shared_ptr by value
std::cout << "Reference Count after call: " << ptr5.use_count() << "\n";
ptr5->show();
}
return 0;
}
Passing the shared_ptr by value to the function increases the reference count. If we pass it by reference though the count will not increase.
std::weak_ptr
The weak_ptr is a smart pointer that holds a non-owning reference to an object managed by a shared_ptr. The resource cannot be directly accessed through the weak_ptr, but we have to convert it to a shared_ptr first.
Having a weak_ptr is safer than using raw pointers because we can tell is the resource referenced has been release prior to accessing it and we do not increase its reference count, thus allowing for its release. This feature is very helpful in complex memory management schemes that may occur in large applications that often lead to cyclic resource reference and eventually to memory leaks.
We start with a simple weak_ptr example:
int main()
{
std::cout << "Hello from part 13!\n";
// using weak_ptr
std::cout << "------------------ using weak_ptr\n";
{
std::weak_ptr<Sample> weakPtr; // Using std::weak_ptr
{
std::cout << "----- weak_ptr lock count -----\n";
std::shared_ptr<Sample> ptr7(new Sample(7)); // Using std::shared_ptr
weakPtr = ptr7; // Assign shared_ptr to weak_ptr
std::cout << "Reference Count with weak_ptr: " << ptr7.use_count() << "\n";
if (auto sharedPtr = weakPtr.lock()) // Convert weak_ptr to shared_ptr
{
sharedPtr->show();
std::cout << "Reference Count inside lock: " << sharedPtr.use_count() << "\n";
}
std::cout << "Reference Count after lock: " << ptr7.use_count() << "\n";
}
// ptr7 goes out of scope here so the lock will fail
if (auto sharedPtr = weakPtr.lock()) // try to convert weak_ptr to shared_ptr
{
sharedPtr->show();
std::cout << "Reference Count inside lock: " << sharedPtr.use_count() << "\n";
}
else
{
std::cout << "The object has been destroyed.\n";
}
}
return 0;
}
Notice how weakPtr.lock fails after the shared_ptr holding the reference to the object goes out of scope.
Here is an example of cyclic refences where the use of weak_ptr is mandatory:
class A;
class B;
class A {
public:
std::shared_ptr<B> b_ptr; // Use shared_ptr to create circular dependency
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
class C;
class D;
class C {
public:
std::weak_ptr<D> d_ptr; // Use weak_ptr to avoid circular dependency
~C() { std::cout << "C destroyed\n"; }
};
class D {
public:
std::shared_ptr<C> c_ptr;
~D() { std::cout << "D destroyed\n"; }
};
int weak_test() {
std::cout << "----- weak_test -----\n";
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // Strong reference
b->a_ptr = a; // Strong reference
}
std::cout << "objects A and B are not destroyed when they go out of scope\n";
{
std::shared_ptr<C> c = std::make_shared<C>();
std::shared_ptr<D> d = std::make_shared<D>();
c->d_ptr = d; // Weak reference
d->c_ptr = c; // Strong reference
}
std::cout << "objects are properly destroyed when they go out of scope\n";
return 0;
}
When to use Smart Pointers
unique_ptr for exclusive ownership
shared_ptr when multiple owners are needed
weak_ptr to have access to shared resources without affecting their lifetime
Summary
In this part we explored the C++ standard library classes we can use to efficiently handle dynamically allocated memory.