Understanding Smart Pointers in C++
Written on
Smart pointers play a crucial role in C and C++ programming, providing direct access to memory while managing it efficiently.
What is a Pointer?
Pointers are variables that store memory addresses of other variables. They enable indirect access to a variable's value, allowing us to modify it without needing to copy data, which can slow down application performance. Pointers facilitate quicker memory access, particularly beneficial in real-time applications where time-sensitive operations are critical. There are two primary types of pointers: raw pointers and smart pointers.
Raw Pointers
A raw pointer is the conventional pointer we are familiar with. However, it can introduce various issues. Memory allocation and deallocation occur dynamically using the new and delete keywords with raw pointers. If allocated memory isn't properly deallocated, it results in memory leaks. Memory leaks occur when allocated memory is no longer needed, potentially leading to out-of-memory errors, reduced performance, or application crashes. Effective dynamic memory management is essential when using raw pointers.
To illustrate, consider the following example demonstrating a class with a constructor and destructor to observe pointer lifespan.
#include <iostream>
using namespace std;
class Source {
public:
Source() { cout << "Source Constructed." << endl; }
~Source() { cout << "Source Deconstructed." << endl; }
};
int main() {
Source* p = new Source;
delete p;
return 0;
}
The output will be: Source Constructed. Source Deconstructed. If the delete keyword is omitted, the output would only indicate "Source Constructed," resulting in a memory leak. Unlike smart pointers, raw pointers do not automatically deallocate memory.
Smart Pointers
Smart pointers help mitigate memory leak issues. There are three main types of smart pointers.
- unique_ptr
Introduced in C++11, unique_ptr replaced auto_ptr, which was removed due to complications. This smart pointer manages the lifetime of arrays and objects, ensuring that only one unique_ptr can own an object at any time. It addresses problems related to dangling pointers (pointers that reference invalid data) and memory leaks, eliminating the need for manual dynamic memory management. When the pointer goes out of scope, the memory it owns is automatically released. Ownership can be transferred to another pointer, but it cannot be copied or shared. Move semantics are employed for ownership transfer. Additionally, C++14 introduced the make_unique function, allowing the creation of unique_ptr without the new keyword.
Replacing the pointer with unique_ptr in the main code retains the same output, even without the delete keyword.
#include <memory> // unique_ptr is found in the memory header
int main() {
unique_ptr<Source> ptr1 = make_unique<Source>(); // Preferred over new
unique_ptr<Source> ptr2 = move(ptr1); // Ownership transferred from ptr1 to ptr2
cout << "Ptr1 is " << (ptr1 ? "not null.n" : "null.n");
cout << "Ptr2 is " << (ptr2 ? "not null.n" : "null.n");
return 0;
}
The output: Source Constructed. Ptr1 is null. Ptr2 is not null. Source Deconstructed.
- shared_ptr
Unlike unique_ptr, shared_ptr allows multiple pointers to share ownership of an object. The object is only destroyed when the last pointer referencing it is deallocated. Each time a new pointer pointing to the same object is created, a reference counter increases, and when a pointer goes out of scope, the counter decreases. Memory is released only when the reference count reaches zero. For multiple pointers, a copy of the original pointer should be used. The make_shared function is available for creating shared_ptr, similar to make_unique.
int main() {
shared_ptr<Source> ptr1 = make_shared<Source>();
{
shared_ptr<Source> ptr2 = ptr1; // Shared ownership}
cout << "Out of scope." << endl;
return 0;
}
The output: Source Constructed. Out of scope. Source Deconstructed. In this example, the shared_ptr is only deallocated after the last pointer has exited its scope. However, this can lead to a problem known as "cyclical ownership."
Consider the following code changes:
class Source {
public:
shared_ptr<Source> src_ptr{};
Source() { cout << "Source Constructed." << endl; }
~Source() { cout << "Source Deconstructed." << endl; }
};
int main() {
shared_ptr<Source> ptr = make_shared<Source>();
ptr->src_ptr = ptr; // Creates a cycle
return 0;
}
The output will only show "Source Constructed." since the pointer won't deallocate. ptr and src_ptr share the object, and src_ptr doesn't go out of scope, causing ptr to remain in memory until reassigned.
- weak_ptr
weak_ptr is intended to resolve the issue of "cyclical ownership." It can access an object owned by a shared_ptr without increasing the reference count, allowing for timely memory deallocation without waiting for all pointers to exit their scopes.
int main() {
weak_ptr<Source> weak;
{
shared_ptr<Source> shared = make_shared<Source>();
weak = shared;
}
cout << "Out of scope." << endl;
return 0;
}
The output: Source Constructed. Source Deconstructed. Out of scope. In this instance, the object is destroyed before exiting the second scope, unlike shared_ptr. However, this can lead to "dangling pointers," similar to raw pointers. weak_ptr includes two functions for checking the existence of an object, expired and lock. The use_count function can also be used to determine how many references to the object exist before it is destroyed.
int main() {
weak_ptr<Source> weak;
{
shared_ptr<Source> shared = make_shared<Source>();
weak = shared;
cout << "Number of users: " << weak.use_count() << 'n';
cout << "Object " << (weak.lock() ? "still exists.n" : "does not exist.n");
}
cout << "Object " << (weak.expired() ? "does not exist.n" : "still exists.n");
return 0;
}
The output: Source Constructed. Number of users: 1 Object still exists. Source Deconstructed. Object does not exist. The object is destroyed after exiting the scope because only one user (the shared_ptr) remained. weak_ptr functions solely as an observer.
References: - https://www.learncpp.com/cpp-tutorial - https://roadmap.sh/cpp