Smart Pointers in C++ - Part 3
Welcome to Part #3 of Smart Pointers in C++!
The Parts #1 and #2 are a prerquisite for Part #3 and can be found here
Let's learn more about shared_ptr
.
We mention earlier in Part 2 that shared_ptr
are reference-counted smart pointers. What does that really mean? It means, that every shared_ptr
object has two raw pointers encapsulated in it. One raw pointer points to the actual owned resource and the other raw pointer points to a shared control block that stores the count of the current co-owners (shared_ptr
objects) of the owned resource. This count is incremented every time a new shared_ptr
becomes a co-owner of the resource. The count is decremented every time a co-owner goes out-of-scope or releases the ownership of the resource in some way. When the count reaches 0
, the control block deletes the resource (freeing up the memeory) and itself.
shared_ptr
Usage scenario for Assume, we have a Person
user-defined class. The Person
class has two data members fullname
and age
. The attribute fullname
is of type Name
, which is another user-defined class and age
is of type int
. Let's have a look at how the code might look.
The declaration for Name
class Name {
private:
string fname;
string lname;
public:
Name();
Name(string, string);
string getFname() const;
string getLname() const;
void setFname(string);
void setLname(string);
};
The declaration for Person
class Person {
private:
Name fullname;
int age;
public:
Person();
Person(Name, int);
Name getFullname() const;
int getAge() const;
void setFullname(Name);
void setAge(int);
};
What are some problems with this impementation?
Background information
The fullname
attribute of the Person
class is of type Name
. In other words, fullname
is an object of Name
. So the Name
object is contained inside the Person
object.
Problems
Have a look at the 2-arg constructor and the accessor and mutators for fullname
in the Person
class. The constructor Person(Name, int)
takes a Name
object as the first argument. This argument is passed by copy (You must certainly know what pass-by-copy means). Which means, the copy constructor of Name
is called and a copy of the object is created when the constructor gets called. Moreover, there will be another copy of the object created by a call to copy assignment of Name
.
Person::Person(Name n, int a) {
fullname = n; // copy assignment
age = a;
}
Though the call to copy assignment could be easily avoided using initializer lists.
Person::Person(Name n, int a) : fullname(n), age(a) {
//
}
But this does not eliminate the call to the copy constructor of Name
(when n
's value is assigned to fullname
).
Same applies to the mutator of fullname
.
void Person::setFullname(Name n) {
fullname = n; // copy assignment
}
As the argument is pass-by-copy, the copy constructor of Name
is called; and then copy assignment when the fullname = n;
statement executes.
The accessor Name getFullname() const
returns the Name
object by copy.
Name Person::getFullname() {
return this->fullname;
}
So the copy constructor of Name
is called for creating a copy of the object when it gets returned.
Do you get the idea here? Every time a copy constructor or copy assignment gets called, a copy of the Name
object gets created in the memory. Not very memory efficient, right?
Possible (not so safe) solution
We could change the Person
class to use pointer for fullname
.
class Person {
private:
Name* fullname;
int age;
public:
Person();
Person(Name*, int);
Name* getFullname() const;
int getAge() const;
void setFullname(Name*);
void setAge(int);
};
Now we do not have the problem of creating multiple copies of the Name
object. We are passing around a raw pointer to the Name
object. So there are no copies created.
But, since we are using raw pointers, all the problems associated with raw pointers mentioned in Part 1 now arise. For example, one could delete
the Name
object from outside the Person
class. Consider the following code snippet.
void someFunction() {
Person p("Harry", "Potter", 11);
// try getting a pointer to the Name object
Name* harry = p.getFullname();
// some more code that uses harry
delete harry;
}
Now, we just deleted the Name
object which was contained inside the Person
object p
. So the Name
pointer that we have in Person
, viz. fullname
is now indeterminate — it doesn't point to any valid memory. Moreover, when the Person
object p
goes out of scope, the destructor of Person
will run. The destructor will also have a delete fullname;
statement in its body. When that statement runs, we get a segmentation fault as fullname
is not pointing to a valid Name
object. This is something that we do not want to happen.
Better solution
You guessed it right! We use smart pointers.
Since we want to pass around the pointer of the Name
object, it's a perfect scenario to use shared_ptr
. We can't use unique_ptr
as it would not allow us to have multiple smart pointers own the same resource.
So now the Person
class will look like so
class Person
{
private:
std::shared_ptr<Name> fullName;
int age;
public:
Person();
Person(string fn, string ln, int a);
std::shared_ptr<Name> getFullName() const;
int getAge() const;
};
We may use the accessor getFullName()
like so
void someFunctionAboutSharedPtr() {
// create an object of Person
std::unique_ptr<Person> pPerson (new Person("Jane", "Doe", 28));
// call the accessor
std::shared_ptr<Name> pName = pPerson->getFullName();
std::cout << "First name: " << pName->getFname() << '\n'; // Jane
std::cout << "Last name: " << pName->getLname() << '\n'; // Doe
std::cout << "Age: " << pPerson->getAge() << '\n'; // 28
// reset the pName smart pointer
pName.reset(); // won't delete the actual Name object
// try and get a pointer to the same Name object again
std::shared_ptr<Name> pName2 = pPerson->getFullName();
std::cout << "First name: " << pName2->getFname() << '\n'; // Jane
std::cout << "Last name: " << pName2->getLname() << '\n'; // Doe
}
This is a better solution because we get all the advantages of smart pointers. When sharing the Name
object with the outside world, we don't have to worry about freeing up the memory. Even if some code outside the class accidentally resets the shared_ptr
, the actual Name
object won't be deleted as there's still at least one shared_ptr
(the one inside the Person
class) that still owns the Name
object. In fact, there is no way of accidentaly deleting the contained Name
object from outside the class. The most we can do is reset the shared pointer outside the class which won't delete the contained Name
object. The only time when the Name
object gets deleted is when the destructor for Person
class runs.
So my friends, please ditch the raw pointers and start using smart pointers!
Click here to know more about shared_ptr
.