Smart Pointers in Modern C++

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:

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class Dog{
string m_name;
public:
Dog(string name){cout << "Dog is created: "<< name <<endl; m_name = name;}
Dog(){cout << "Nameless dog created. " << name <<endl; m_nam = "nameless";}
~Dog(){cout << "Dog is destroyed: " << m_name <<endl;}
void bark(){cout << "Dog " << m_name << "rules!" << endl;}
};

view raw
CPP_SmartPtr_01.cpp
hosted with ❤ by GitHub

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.

//Hard to track when to delete pointer in large project
//We need to free the memory at the right step!
void foo(){
Dog* p = new Dog("Gunner");
//
delete p;
//
p->bark(); // p is a dangling pointer now — undefined behavior
}// If we do not delete p, then we will have memory leak

view raw
CPP_SmartPtr_02.cpp
hosted with ❤ by GitHub

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:

void fooFunc(){
//We will have a count to keep track of how many pointers are pointing to the object.
shared_ptr<Dog> p(new Dog("Gunner")); // count == 1 now
//This is not allowed
//shared_ptr<Dog> pt = new Dog("Smile");
//Returns the raw pointer.
//In general, avoid using raw pointer when use smart pointers
Dog* ptr = p.get();
//We can dereference the smart pointer just like the raw pointer
(*p).bark();
//Use the shared pointer just like the raw pointer.
p->bark();
{
shared_ptr<Dog> p2 = p; //count == 2
p2->bark();
cout << p.use_count() << endl; //Output how many pointers we have
}
p->bark(); //count == 1
}// count will be 0 when code executes to here, and Gunner will be deleted

view raw
CPP_SmartPtr_03.cpp
hosted with ❤ by GitHub

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:

void foo_01Func(){
Dog* d = new Dog("Tank"); //Should not use
shared_ptr<Dog> p(d); // p.get_count() == 1
//Here when p goes out of scope, d will be destroyed.
//Then p2 goes out of scope, p2 will be destroyed again…
shared_ptr<Dog> p2(d); // p2.get_count() == 1, the counter won't increase
/* Lesson: An object should be assigned to a shrared_pointer
immediately when it is created. The above case does not follow
this rule. We first create the raw pointer d and then initialize
p and p2 with d. We should do somthing like:
shared_ptr<Dog> p(new Dog("Tank")), then shared_ptr<Dog> p2 = p.*/
}

view raw
CPP_SmartPtr_04.cpp
hosted with ❤ by GitHub

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.

void booFunc(){
//Another way to create a shared pointer:
//Faster and safer/ Exception safe
shared_ptr<Dog> p = make_shared<Dog>("Ink");
shared_ptr<Dog> p1 = make_shared<Dog>("Gunner");
shared_ptr<Dog> p2 = make_shared<Dog>("Tank");
// In the following situation, Gunner is deleted
p1 = p2;
//p1 = nullptr;
//p1.reset();
//Sometimes we have to use constructor to create shared pointer
//instead of make_shared<class>(). We will take a look at below:
//using default deleter: operator delete
shared_ptr<Dog> p3 = make_shared<Dog>("Shooter");
//Define our own custome deleter
shared_ptr<Dog> p4 = shared_ptr<Dog>(new Dog("Tank"), [](Dog* p){cout << "Custome deleting."; delete p;});
//Dog[1] and Dog[2] have memory leak.
shared_ptr<Dog> p5(new Dog[3]);
//All 3 dogs will be deleted when p goes out of scope
shared_ptr<Dog> p6(new Dog[3], [](Dog* p){delete[] p;});
}

view raw
CPP_SmartPtr_05.cpp
hosted with ❤ by GitHub

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:

class Dog {
shared_ptr<Dog> m_pFriend;//cyclic reference
//weak_ptr<Dog> m_pFriend; //Solution to fix the problem
public:
string m_name;
Dog(string name) : m_name(name){cout << "Dog: " << m_name << " is defined!" << endl;}
void bark() {cout << "Dog " << m_name << " rules!" << endl;}
~Dog() {cout << "Dog is destroyed: " << m_name << endl;};
void makeFriend(shared_ptr<Dog> f){m_pFriend = f;}
};
int main(){
shared_ptr<Dog> pD(new Dog("Gunner"));
shared_ptr<Dog> pD1(new Dog("Smile"));
pD->makeFriend(pD1);
pD1->makeFriend(pD);
//In this code, Dog destructor will not be called, when pD goes out of scope,
//pD1 still has a pointer points to pD; when pD1 goes out of scope, pD still
//has a pointer points to pD1
//Will cause memory leak: cyclic reference!
//using weak pointer to declare m_pFriend.
//Weak pointer has no ownership of the pointed object.
return 0;
}

view raw
CPP_SmartPtr_06.cpp
hosted with ❤ by GitHub

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:

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class Dog {
weak_ptr<Dog> m_pFriend;
public:
string m_name;
Dog(string name) : m_name(name) { cout << "Dog: " << m_name << " is defined!" << endl; }
void bark() { cout << "Dog " << m_name << " rules!" << endl; }
~Dog() { cout << "Dog is destroyed: " << m_name << endl; };
void makeFriend(shared_ptr<Dog> f) { m_pFriend = f; }
void showFriend() {
//Compile error. m_pFriend cannot direct access the object like the normal pointer.
//cout << "My Friend is: " << m_pFriend->m_name << endl;
// The following code fix the problem. lock() function will create
//a shared_ptr of weak_ptr. It will check whether the weak pointer
//is still pointing to a valid object, and make sure that when the
// weak pointer is accessing the object, the object has not been
//deleted!
//If the weak pointer is empty, the lock() will throw an exception.
if (!m_pFriend.expired())
cout << "My Friend is: " << m_pFriend.lock()->m_name << endl;
cout << "He is owned by " << m_pFriend.use_count() << " pointers" << endl;
}
};
int main() {
shared_ptr<Dog> pD(new Dog("Gunner"));
shared_ptr<Dog> pD1(new Dog("Smile"));
pD->makeFriend(pD1);
pD1->makeFriend(pD);
pD->showFriend();
return 0;
}

view raw
CPP_SmartPtr_07.cpp
hosted with ❤ by GitHub

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:

void test(){
Dog* pD = new Dog("Gunner");
pD->bark();
//If we return here, or some exceptions happen here
//pD will cause memory leak!
delete pD;
}
//Using unique pointer here
void test01(){
unique_ptr<Dog> pD (new Dog("Gunner"));
pD->bark();
//If we return earlier, or some exceptions happen here
//pD will not cause memory leak!
}

view raw
CPP_SmartPtr_07.cpp
hosted with ❤ by GitHub

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:

void test02(){
//unique pointer can access the same object at different time
unique_ptr<Dog> pD (new Dog("Gunner"));
unique_ptr<Dog> pD1 (new Dog("Smile"));
pD1->bark();
//Smile will be destroyed, and pD1 owns pD now
pD1 = move(pD);
pD1->bark();
//release() function will return the raw pointer. It will also
//change the ownership of unique pointer and set it to nullptr.
Dog* p = pD1.release();
//reset pD to other object, if unique pointer originally owns an
//object, that object will be deleted!
//pD.reset(new Dog(smokey));
//same as pD = nullptr
//pD.reset();
if(!pD1) { // pD1 is empty now
cout << "pD1 is empty now." <<endl;
}
}

view raw
CPP_SmartPtr_08.cpp
hosted with ❤ by GitHub

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:

void foo(unique_ptr<Dog> p){
p->bark();
}
unique_ptr<Dog> getDog(){
unique_ptr<Dog> p(new Dog("Jack"));
//return p will use the move semantics!
//so p will no longer has the ownership of Jack
return p;
}
void test03(){
unique_ptr<Dog> pD (new Dog("DND"));
//Cannot directly pass to foo(), since pD is unique pointer.
//pD will be destroyed in foo().
foo(move(pD));
//pD2 owns the jack now, so it's not nullptr!
unique_ptr<Dog> pD2 = getDog();
if(!pD2) {cout << "pD2 is nullptr. " << endl;}
}

view raw
CPP_SmartPtr_09.cpp
hosted with ❤ by GitHub

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:

//using customized deleter
shared_ptr<Dog> pSD(new Dog[3], [](Dog *p){delete []p;});
//for unique pointer, we do not need customized deleter.
//Need to indicate it in template parameter: unique_ptr<Dog[]>.
unique_ptr<Dog[]> dogs(new Dog[3]);

view raw
CPP_SmartPtr_10.cpp
hosted with ❤ by GitHub


That’s all about the smart pointers in modern C++. Thank you for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s