In this article, we are going to briefly discuss three smart pointers in modern C++. Basically, there are three types of smart pointers in C++: shared pointer, unique pointer, and weak pointer. Let’s start with the shared pointer.
1. Shared Pointer
When we use raw pointers, one critical problem is that we need to manage memory allocation. It’s a common case that in a large project, deleting the memory too early or too late will cause problems. Let’s first define a class dog like below:
Then we define a function foo() and call this function in main() function, what will happen? We will try to dereference a dangling pointer p, it will cause undefined behavior. Then what if we comment p, we may end up with memory leak, because we forget to delete p in the end.
You can see maintaining when to delete a pointer is tedious and error-prone. It will fantastic that we have a pointer that will automatically deallocate the memory when no pointer pointing to the dynamically created object? That’s the reason we will use smart pointers. Let’s first take a look at how to use a shared pointer:
The main advantage of using a shared pointer is that when all the pointers pointing to a dynamically allocated object goes out of scope, the last shared pointer will be responsible for deallocating the allocated memory. In order to achieve this, the std::shared_ptr<> class needs to maintain a counter to count how many pointers are currently pointing to the same object. When the counter goes to 0, we know that no pointer is going to point to this object, the shared pointer will deallocate the memory.
In general, the usage of a shared pointer is similar to a raw pointer. If we have a shared pointer called p, you can dereference the pointer using *p, or you can access the public members of the object using p->bark(). However, you cannot use the assignment operator to assign a newly created object to a shared pointer. You should use the copy constructor instead. You can call the use_count() method to check how many shared pointers are currently pointing to an object. Let’s look at the sample code below:
Note in the above code, we do not need to delete dog gunner because the shared pointer will do this for us. Cheers!
There is a general rule that when using smart pointers, avoid using raw pointers at all. Because it will cause problems. Let’s look at an example below:
C++ standard template library provides another way for us to create a shared pointer. We can call the make_shared<> () method to create a shared pointer. In general, this method is preferred, because it is faster and safer. However, creating a shared pointer by copy constructor is still needed if we want to create our own customized deleter.
Another note we have to mention here is that when we change the shared pointer to point to another object, or we reset the shared pointer to nullptr, it will automatically deallocate the memory. We will cover these rules in the following sample code.
That’s all for the shared pointer, next we will discuss the unique pointer.
2. Weak Pointer
A weak pointer acts really like the raw pointer. However, there are two main differences between a weak pointer and a raw pointer:
The first one is that a weak pointer provides one level of protection: delete operation to a weak pointer is forbidden! If the object the weak pointer points to gets deleted, the weak pointer will become an empty pointer (nullptr)!
The second one is that we cannot do the normal pointer operations on a weak pointer. To be more specific, the operations like directly dereferencing a weak pointer (*p) or using the access syntax to get access to the public members of the object (p->bark()) are forbidden.
Another thing that we need to pay attention to is that a weak pointer does not have ownership of the object it points to. Here the ownership means that the weak pointer is not responsible for allocating and deleting the object it points to, which also means that a weak pointer can only access and modify the content of the object, but it cannot delete the object.
From the above description, we know that a weak pointer is cheaper compared with a shared pointer, so a natural question arises: why do we need weak pointer? Let’s look at an example:
In the above example, when we call pD->makeFriend(pD1), we are actually copying the shared pointer pD1 to m_pFriend in pD. This will increase the counter of Dog Smile to be 2. Then what will happen next? When pD1 goes out of scope and destroyed, the Dog Smile will not be deleted because, at the current stage, the counter of Dog Smile is still 1. The same happens with pD1->makeFriend(pD). We can simply replace the shared pointer with a weak pointer to solve this issue.
We have already mentioned that a weak pointer cannot directly access the member of the object. We need to access the content using lock() method. The following is an example:
Some other common APIs for the weak pointer include:
expired() – Check whether the weak pointer is still valid (whether it is not null).
use_count() – Check how many shared pointers are still pointing to the object.
3. Unique Pointer
A unique pointer is an exclusive ownership, lightweight smart pointer. The exclusive means that we can not let two unique pointer point to the same object, they are mutually exclusive. Lightweight means that a unique pointer is cheaper compared with a shared pointer because there is no need to keep track of pointer counters.
The definition and usage of a unique pointer are similar to shared pointer except that unique pointer has exclusive ownership. If we know that in some cases, we will never have more than one pointer pointing to the same object, a unique pointer is typically preferred.
For example, we can use the unique pointer in the function to prevent memory leak:
One of the critical things we need to keep in mind that a unique pointer is easy to convert to a shared pointer, while a shared pointer cannot be converted to a unique pointer. To be more specific, we cannot directly assign a shared pointer to a unique pointer, while we can use std :: move() method to convert a unique pointer to a shared pointer. The following code converts a unique pointer to a shared pointer:
std::unique_ptr<std::string> unique = std::make_unique<std::string>("test"); std::shared_ptr<std::string> shared = std::move(unique); //or std::shared_ptr shared = std::make_unique("test");
We already know that we cannot have two unique pointers pointing to the same object, however, a unique pointer can access the same object at a different time. We can use the move() method to change the ownership of one unique pointer and assign it to another unique pointer. We can also get the raw pointer by using release() member function. After calling this function, the unique pointer will release the ownership and be set to nullptr. Some examples can be found below:
We need to be careful if we want to pass the unique pointer to another function. The following example code explains that passing a unique pointer to a function and return a unique pointer by a function:
The next thing about the unique pointer is that we cannot construct a unique pointer from a weak pointer, and we cannot construct a weak pointer from a unique pointer. The detailed explanation about why we cannot do this is from here.
The last thing about the unique pointer is related to the constructor, please look at the code below:
That’s all about the smart pointers in modern C++. Thank you for reading.