For Enterprise software, reliable and efficient code with a robust memory architecture is critical. In this article, we will learn about how smart pointers have improved memory management in C++.
The Problem with Raw Pointers in Enterprise Applications
C++ is still one of the fastest languages to make an application. However, proper memory management remains one of the critical challenges. Manual memory management offered by c++ by the use of raw pointers, while offering a very deep control over the memory, is prone to errors. Such as:
- Memory Leaks: Forgetting to call ‘delete’ on dynamically allocated memory.
- Dangling Pointers: Accessing memory after it has been deallocated.
- Double Deletion: Attempting to delete the same memory twice, leading to heap corruption.
- Exception Safety: If an exception is thrown between ‘new’ and ‘delete’, the memory may never be released.
Smart Pointers
These pointers are smart in the sense that they manage memory automatically. C++11 introduced std::unique_ptr, std::shared_ptr, and std::weak_ptr, for better modern C++ memory management.
1) std::unique_ptr
This pointer ‘owns’ the object it is pointing to. This means, only one unique_ptr can point to a particular object. The object cannot be copied but can be moved. Being a smart pointer, it automatically deletes the object when it goes out of scope.
Code Snippet
#include <iostream> #include <memory> using namespace std; class Resource { public: Resource() { cout << "Resource acquired\n" << "endl"; } ~Resource() { cout << "Resource released\n" << "endl"; } }; int main() { unique_ptr<Resource> ptr1 = make_unique<Resource>(); unique_ptr<Resource> ptr2 = move(ptr1); return 0; }
Output
Resource acquired Resource released
Explaination
- make_unique<Resource>(), dynamically allocates a Resource object on the heap and returns a unique_ptr that exclusively owns this Resource object.
- The addition of the ‘memory’ library gives us access to the smart pointers.
- ptr1 becomes the unique owner of the Resource object. “Resource acquired” is printed.
- ‘move’ here is used to transfer ownership from ptr1 to ptr2. It is important to note the Resource in neither destroyed or recreated here.
- After the ‘main’ function ends, the Resource is release automatically by the smart pointer and “Resource released” is printed.
2) std::shared_ptr
This smart pointer allows users to share the ownership of an object. Unlike unique_ptr, which only allows one unique_ptr to point to an object, multiple shared_ptr can point to the same object. This means the object is not deleted until all the shared_ptr pointing to it are deleted. This functionality allows us to make a copy of this pointer.
This pointer keeps track of the number of how many shared_ptr are tracking an object by internally keeping track of a “reference count” variable, which automatically changes count on destruction or creation.
Code Snippet
#include <iostream> #include <memory> using namespace std; class Data { public: Data() { std::cout << "Data created" << endl; } ~Data() { std::cout << "Data destroyed" << endl; } }; void use_data(std::shared_ptr<Data> d) { std::cout << "Using data, ref count: " << d.use_count() << endl; } int main() { std::shared_ptr<Data> s_ptr1 = std::make_shared<Data>(); std::shared_ptr<Data> s_ptr2 = s_ptr1; use_data(s_ptr2); cout<< "Actual ref count: " <<s_ptr2.use_count()<<endl; return 0; }
Output
Data created Using data, ref count: 3 Actual ref count: 2 Data destroyed
Explaination
- make_shared<Data>(), allocates a Data object on the heap and manages it. It also initialises an internal “reference count” to 1. s_ptr1 is assigned this value.
- When the shared Pointer s_ptr1 is assigned a Data, the reference count is set to 1 and “Data created” is printed.
- After s_ptr2 makes a copy of sptr1, the reference count increases to 2.
- When s_ptr2 is passed into the function ‘use_data’, another copy of it is passed because it is not passed by reference, increasing the reference count to 3.
- This results in an output of ref count as 3. Whereas, the actual ref count printed afterwards stays at 2.
- As the program ends, the smart pointer automatically destroys the objects, and the destructor is called, which prints “Data destroyed”.
3) std::weak_ptr
It acts as a shared_ptr without increasing the ‘reference count’. The common use of this is to combat the problem of cyclic dependency. Say, one object1 has a shared_ptr to object2, and object2 has a shared_ptr to object1. In this scenario, the reference count of both will never become zero, causing a memory leak. To combat this, we can use weak_ptr for one of them. This pointer has a function called ‘lock()’, which returns a shared_ptr to the object, if it is still alive or a null pointer if not.
Code Snippet
#include <iostream> #include <memory> using namespace std; class Node { public: weak_ptr<Node> next; Node() { cout << "Node created" << endl; } ~Node() { cout << "Node destroyed" << endl; } }; int main() { shared_ptr<Node> node1 = make_shared<Node>(); shared_ptr<Node> node2 = make_shared<Node>(); node1->next = node2; node2->next = node1; auto locked_node1 = node1->next.lock(); if (locked_node1) { cout << "node1's next still exists." << endl; } else { cout << "node1's next is gone." << endl; } return 0; }
Output
Node created Node created node1's next still exists. Node destroyed Node destroyed
Explaination
- node1 becomes a shared pointer to a Node object. “Node created” is printed. Reference count = 1.
- node2 becomes a shared pointer to a Node object. “Node created” is printed. Reference count = 1.
- node1’s next is pointed to node2.
- node2’s next is pointed to node1.
- node1->next.lock(), allows us to check if node1->next (which is a shared pointer), has something it’s pointing towards. In this yes, it it true, as it is pointing to node2.
- If condition is executed and “node1’s next still exists.” is printed.
- At this point, node2 Reference count is temporarily 2. After locked_node1 goes out of scope, it drops back to 1.
- The program ends, node1 and node2 goes out of scope. The reference count for both drops to zero and “Node destroyed” is printed for each of them.
Common Mistakes to Avoid
- Mixing Raw Pointers with Smart Pointers: Do not use both a raw pointer and a smart pointer to manage the same object. It can cause bugs like double deletion or memory corruption.
- Ignoring weak_ptr lock return value: Before using a weak_ptr, check if lock() returns a valid shared_ptr. If the object has already been deleted, lock() will return nullptr, and using it without checking can crash your program.
Conclusion
Smart pointers have provided us with a better way to write memory-safe code by eliminating the need for manual deletion calls, preventing memory leaks like dangling pointers and helping us build a robust, maintainable and scalable system.