Codementor Events

Smart Pointers in C++ - Part 3

Published Jun 27, 2021Last updated Jun 28, 2021
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.

Usage scenario for shared_ptr

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.

Discover and read more posts from Sandesh Patil
get started