Smart pointers are classes which compose classes that are created by dynamic allocation and handles deallocation of the memory used the dynamic allocated classes.
When an object is created dynamically, the work of deallocating the memory when the object goes out of scope, goes to our plate and we might forget to clean up things. To solve this issue, smart pointers are created where they compose those object and when smart pointers go out of scope, they get destroyed along with deallocating the memory for the object.
Let’s understand this with the help of an example:
#include <iostream>
#include <string>
#include <string_view>
class Person
{
int m_age{};
std::string m_name{};
public:
Person(int age, std::string_view name)
: m_age{age}, m_name{name}
{
std::cout << "Creating person" << '\n';
}
~Person()
{
std::cout << "Destroying person" << '\n';
}
void sayHello() const
{
std::cout << "Hi" << '\n';
}
friend std::ostream &operator<<(std::ostream &out, const Person &p)
{
return out << "Name: " << p.m_name << "\nAge: " << p.m_age << "\n";
}
};
int main(int argc, char const *argv[])
{
Person *p{new Person(12, "Hemant")};
std::cout << *p << '\n';
p->sayHello();
return 0;
}When we run this program, we shall see following:
❯ ./smart_pointers.out
Creating person
Name: Hemant
Age: 12
Hi
So, the constructor is called but the destructor is never get called and so the dynamically allocated memory is never released during the program execution. It would lead to memory leaks when this happens multiple times.
We can solve this issue by creating a class that takes the pointer of dynamically allocated memory for the object and delete the memory when that class object gets destroyed.
class SmartPointer
{
Person *m_ptr{};
public:
SmartPointer(Person *ptr)
: m_ptr{ptr} {}
~SmartPointer()
{
delete m_ptr;
}
Person &operator*() const
{
return *m_ptr;
}
Person *operator->() const
{
return m_ptr;
}
};This class called SmartPointer constructor takes the pointer of the dynamic allocated class object. The destructor then deallocates the dynamic allocated memory.
Let’s use this in our code.
#include <iostream>
#include <string>
#include <string_view>
class Person
{
int m_age{};
std::string m_name{};
public:
Person(int age, std::string_view name)
: m_age{age}, m_name{name}
{
std::cout << "Creating person" << '\n';
}
~Person()
{
std::cout << "Destroying person" << '\n';
}
void sayHello() const
{
std::cout << "Hi" << '\n';
}
friend std::ostream &operator<<(std::ostream &out, const Person &p)
{
return out << "Name: " << p.m_name << "\nAge: " << p.m_age << "\n";
}
};
class SmartPointer
{
Person *m_ptr{};
public:
SmartPointer(Person *ptr)
: m_ptr{ptr} {}
~SmartPointer()
{
delete m_ptr;
}
Person &operator*() const
{
return *m_ptr;
}
Person *operator->() const
{
return m_ptr;
}
};
int main(int argc, char const *argv[])
{
SmartPointer p{new Person(12, "Hemant")};
std::cout << *p << '\n';
p->sayHello();
return 0;
}Running this program, we should see:
❯ ./smart_pointers.out
Creating person
Name: Hemant
Age: 12
Hi
Destroying person
So, when SmartPointer object goes out of scope, dynamically created Person object also gets destroyed because of the smart pointer’s destructor.
Problems with our SmartPointer
Consider the following program:
int main(int argc, char const *argv[])
{
SmartPointer p{new Person(12, "Hemant")};
{
SmartPointer q{p};
}
std::cout << *p << '\n';
p->sayHello();
return 0;
}When we run this program, program crashes by giving following output (on my machine):
Creating person
Destroying person
Name:
Age: 1112801904
Hi
Destroying person
smart_pointers.out(28750,0x200ca6e40) malloc: *** error for object 0x7fb: pointer being freed was not allocated
smart_pointers.out(28750,0x200ca6e40) malloc: *** set a breakpoint in malloc_error_break to debug
fish: Job 1, './smart_pointers.out' terminated by signal SIGABRT (Abort)
Why does it happen?…
It happens because when we initialized p to q, SmartPointer uses default copy constructor which does a shallow copy of pointers. So, both p’s and q’s pointer points to the same location. When q goes out of scope, q’s destructor deallocates the dynamic memory and in outside p’s pointer becomes dangled. Accessing this pointer causes unexpected outcomes and crashes the program.
We can solve this problem by doing a deep copy but it would be an expensive operation. This is where move semantic comes into the picture.