Smart Pointer in C++
The purpose of this blog post is to provide an insight into the use of Smart Pointers (Shared Pointers in C++) and the most important basics. The following explanations aim to provide a quick introduction to this topic.
Top!
Hello
The purpose of this blog post is to provide an insight into the use of Smart Pointers (Shared Pointers in C++) and the most important basics. The following explanations aim to provide a quick introduction to this topic.
In the introductory chapter, the necessity of shared pointers is first clarified and their use is motivated. It is not without reason that shared pointers are a standard language feature in some high-level programming languages, such as ARC (Automatic Reference Counting) in Objective-C.
You hear this statement very often. But are shared pointers already really being used on a larger scale and consistently in projects? In discussions with developers, one quickly gets the impression that shared pointers are not particularly widespread or popular. What is the reason for this? Are there problems that occur more frequently during use? Are shared pointers too complicated to understand or is it too much effort to use them?
We hope this post will help clarify some of the questions surrounding the mystery of shared pointers and provide a good example of how to use them correctly.
Instead of jumping straight into technical details, the use of shared pointers is first explained using a real-life example:
Imagine you work in a company and there is a simple rule for the office building: the last employee who leaves the building must lock the entrance door to the building and activate the alarm system. If someone leaves the building and the alarm system is already activated, then the alarm will be triggered. Therefore, you should carefully check if there is someone else in the building before activating the alarm system.
Assuming the office building consists of only one small room and there are only five employees, it is very easy for you to determine if you are the last employee left in the building. However, imagine you are working in a huge office building with several floors and many hundreds of employees. In order to know if you are the last person in the building, you would have to search all floors and all rooms. Now consider, someone else is searching the building at the same time to see if anyone else is in the building and they both miss each other as a result. You end up accidentally locking the other person in the building with the alarm system activated. What a mess!
But what if there was a kind of "smart door" to the building, which adds +1 for each person entering the building and subtracts -1 for each person leaving the building. So as soon as the counter reading reaches 0, the alarm system is automatically activated.
Wouldn't that be a great solution to this problem? The idea behind the Shared Pointers is similar to the "Smart Door" from the example explained.
When programming in C++ it is often not easy to keep track of the memory management of the entire source code of a project. Once a C++ pointer has been created, the following questions often arise:
The simple answer to these questions is usually the following statement:
"The pointer is deleted at the point in the program where it was last used."
But as in the example from 1.2, where it is not possible to determine unambiguously how many people are in a large building and where exactly they are, it is by no means easy to keep track of where all the pointers in a program have been forwarded to.
"How do you know which is the last place a pointer was used?"
A pointer could possibly have been passed on by several constructors or methods parameters. In addition, it could be that one works together with several teammates on the same source code.
Comparing the handling of pointers in C++ with the example from 1.2, calling the delete command on a pointer for which memory has already been freed would be equivalent to trying to leave the building in which the alarm system has already been activated. As soon as one tries to open the door from the inside, the alarm system would be triggered and the program would crash for the pointers.
Which is the last place a pointer was used and knowing at what time to call delete is not
a question that can be answered so easily, as long as there is a third instance that
takes care of this task. And with this third instance Shared
Pointer, like the std::shared_ptr
come into play.
The shared pointer knows exactly which is the last place where a pointer was used and as soon as a pointer was used for the last time in the whole program, it is automatically deleted. This happens every time someone creates a copy, e.g. when the shared pointer is passed as a parameter in a method or in a constructor. Furthermore, -1 is always subtracted if the object that used the shared pointer has been deleted or the method that used the shared pointer has been terminated. As soon as the value 0 is reached, the pointer is automatically deleted and the occupied memory is released again.
In the following some code examples are presented. Due to the clarity, the office example from chapter 1.2 is taken up again here:
First, an OfficeBuilding class is defined, i.e. a building that can be entered by employees. This means that an OfficeBuilding can reference multiple instances of an Employee object. And as a small, additional feature, there is a washroom in the building, which can be entered by exactly one employee.
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
The left side of the assignment shared_ptr<Employee>
employee1
holds an instance of the Employee class and instead of using the
new keyword, you use the make_shared keyword to create that instance.
The parameters that one would normally pass to the constructor of the Employee class are
instead passed to the expression shared_ptr<Employee>
(parameter1, parameter2, ...)
.
As soon as the current context is left, e.g. the context of the currently called method,
the memory that was occupied by the employee1
instance
is automatically freed. The call of the delete command is therefore not necessary.
Suppose you want to access the name of the variable employee1
using the getter method, you could do this
by treating the variable like a normal C++ pointer and using the arrow operator "->":
string employeeName = employee1->getName();
In contrast, using the dot operator "." grants access to useful information about the shared reference, such as the number of reference counts:
long employee1UseCount = employee1.use_count();
Theoretically, it is also possible to access the raw pointer, which is managed by the shared pointer, via the shared pointer:
Employee *pointer = employee1.get();
However, access to the raw pointer, as in this example, should be avoided at all costs, because this again only leads to well-known problems: someone tries to free memory that has already been freed. This possibility should be mentioned here only for the sake of completeness.
How to make a copy of a shared_ptr which points to allocated memory? Comparing it to normal pointers, one would normally write the following:
Employee *employee1 = new Employee( 1, “Peter Parker”);
Employee *employee1Copy = employee1;
The first line of code allocates an Employee object and has the pointer employee1
point to the newly allocated memory. Then
the second line makes a copy of this variable and the new variable employee1Copy
then points to the same memory area as
employee1
. If you want to do something similar with a
shared_ptr, then there are two possibilities:
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
1) shared_ptr<Employee> employee1Copy = employee1;
2) auto employee1Copy2 = employee1;
Now that the basics about shared_ptr have been covered, it would be interesting next to look at how reference counting behaves in a real code example.
First, we will look at the Employee class. An employee has an attribute name
and a unique id
, which can be passed to the constructor. To keep
the example as simple as possible, this class does not contain any pointers or shared
pointers.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
#import <vector>
#import <string>
#import <iostream>
using namespace std;
class Employee {
private:
int id;
string name;
public:
Employee(int id, const string &name) : id(id), name(name){
}
int getId() const{
return id;
}
const string &getName() const{
return name;
}
void printCurrentUseCount(shared_ptr<Employee> employee){
if(employee.use_count() > 0){
printf("%s, use count: %li n", getName().c_str(), (employee.use_count() -1));
} else{
printf("use count: %li n", employee.use_count());
}
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
To have a better overview of the Reference Counts, there is another method printCurrentUseCount
. Since a new context is entered
with each call of the method and thus the result would be "falsified" by the value +1,
here again the result to be output is always lowered by -1.
The OfficeBuilding, in which the employees work, can be entered and left by calling the
methods enterBuilding and leaveBuilding. On closer inspection, it is noticeable that the
OfficeBuilding class has a class variable vector<shared_ptr<Employee>>
smartDoorPeopleCounter
, which remembers which employees are currently in the
building.
The two methods useWashRoom and leaveWashRoom always allow exactly one employee to enter or leave the WC.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
#include "Employee.h"
#include "WashRoom.h"
#import <vector>
#import <string>
#include <thread>
#include <cstdlib>
class OfficeBuilding {
private:
vector<shared_ptr<Employee>> smartDoorPeopleCounter;
vector<WashRoom> washRoom;
public:
void enterBuilding(shared_ptr<Employee> employeeWhoWantsToEnter) {
smartDoorPeopleCounter.push_back(employeeWhoWantsToEnter);
employeeWhoWantsToEnter->printCurrentUseCount(employeeWhoWantsToEnter);
}
void leaveBuilding(shared_ptr<Employee> employeeWhoWantsToLeave) {
int indexToRemove = -1;
for (int i = 0; i < smartDoorPeopleCounter.size(); i++) {
auto currentEmployee = smartDoorPeopleCounter[i];
if (currentEmployee->getId() == employeeWhoWantsToLeave->getId()) {
indexToRemove = i;
}
}
if (indexToRemove != -1) {
smartDoorPeopleCounter.erase(smartDoorPeopleCounter.begin() + indexToRemove);
}
if (smartDoorPeopleCounter.size() == 0) {
// activate alarm system
}
}
void useWashRoom(shared_ptr<Employee> employee) {
if (washRoom.size() == 0) {
WashRoom emptyWashRoom;
emptyWashRoom.setEmployee(employee);
washRoom.push_back(emptyWashRoom);
} else {
//washroom is currently in use, please wait!
}
}
void leaveWashRoom() {
if (washRoom.size() == 1) {
washRoom.clear();
}
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
The Washroom object always keeps a shared_ptr on the current employee who is currently using the WC.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
#include "Employee.h"
class WashRoom {
shared_ptr<Employee> employee;
public:
void setEmployee(const shared_ptr<Employee> &employee) {
WashRoom::employee = employee;
}
const shared_ptr<Employee> &getEmployee() const {
return employee;
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
The following sample code allocates some employee objects (Employee) and lets them enter and leave the OfficeBuilding.
OfficeBuilding officeBuilding;
/* create employee 1 */
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 1
string employeeName = employee1->getName(); // "Peter Parker"
Employee *pointer = employee1.get(); // attention: raw pointer should not be used!!
shared_ptr<Employee> employee1Copy = employee1;
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 2
auto employee1Copy2 = employee1;
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 3
/* create employee 2 */
shared_ptr<Employee> employee2;
employee2->printCurrentUseCount(employee2); //use count: 0
employee2 = make_shared<Employee>(2, "Bruce Wayne");
employee2->printCurrentUseCount(employee2); //Bruce Wayne, use count: 1
/* create employee 3 */
auto employee3 = make_shared<Employee>(3, "Clark Kent");
employee3->printCurrentUseCount(employee3); //Clark Kent, use count: 1
/* employees entering building */
officeBuilding.enterBuilding(employee1); //Peter Parker, use count: 5
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 4
officeBuilding.enterBuilding(employee2); //Bruce Wayne, use count: 3
employee2->printCurrentUseCount(employee2); //Bruce Wayne, use count: 2
officeBuilding.enterBuilding(employee3);//Clark Kent, use count: 3
employee3->printCurrentUseCount(employee3);//Clark Kent, use count: 2
/* employee 1 entering using washroom */
officeBuilding.useWashRoom(employee1);
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 5
officeBuilding.leaveWashRoom();
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 4
/* employee 1 leaving building */
officeBuilding.leaveBuilding(employee1);
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 3
/* employee 1 visiting coffee house */
CoffeeHouse coffeeHouse;
coffeeHouse.visitCoffeeHouse(employee1);
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 4
bool finish = true;
The print editions here are intended to illustrate what the current Reference Count is in each case:
Peter Parker, use count: 1
Peter Parker, use count: 2
Peter Parker, use count: 3
use count: 0
Bruce Wayne, use count: 1
Clark Kent, use count: 1
Peter Parker, use count: 5
Peter Parker, use count: 4
Bruce Wayne, use count: 3
Bruce Wayne, use count: 2
Clark Kent, use count: 3
Clark Kent, use count: 2
Peter Parker, use count: 5
Peter Parker, use count: 4
Peter Parker, use count: 3
use count inside context: 5
use count outside context: 4
use count from thread: 6
use count after thread finished : 5
Peter Parker, use count: 4
It is suspected that there could be problems related to threads when using C++ shared pointers. Below is also an example for this particular case: there is a cafe, which is located outside the OfficeBuilding and which an employee (Employee) can visit.
The cafe can be entered using the visitCoffeeHouse
method. In this method, first the shared pointer to the employee is assigned to an auto
variable and then to a shared pointer struct, which serves as a container for the
parameters of the thread.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
#include "Employee.h"
#import <vector>
#import <string>
#include <thread>
#include <cstdlib>
#include <unistd.h>
using namespace std;
static pthread_t threadID = pthread_t();
class CoffeeHouse {
private:
shared_ptr<vector<shared_ptr<Employee>>> cafeteriaCounter;
struct threadParams {
shared_ptr<Employee> employeeCopy;
shared_ptr<vector<shared_ptr<Employee>>> cafeteriaCounterCopy;
};
static void *visitCoffeeHouseAsync(void *context) {
unsigned int microseconds = 5000000; // sleep for 5 seconds
usleep(microseconds);
auto params = *(shared_ptr<threadParams> *) context;
auto employee = params->employeeCopy;
string employeeName = employee->getName();
if (employeeName.length() > 0) {
params->cafeteriaCounterCopy->push_back(employee);
printf("use count from thread: %li n", (employee.use_count()));
}
pthread_detach(pthread_self());
return nullptr;
}
public:
CoffeeHouse(){
cafeteriaCounter = make_shared<vector<shared_ptr<Employee>>>();
}
void visitCoffeeHouse(shared_ptr<Employee> employee) {
{
shared_ptr<threadParams> params = make_shared<threadParams>();
params->employeeCopy = employee;
params->cafeteriaCounterCopy = cafeteriaCounter;
printf("use count inside context: %li n", (employee.use_count())); //use count inside context: 5
pthread_create(&threadID, NULL, visitCoffeeHouseAsync, ¶ms);
}
printf("use count outside context: %li n", (employee.use_count())); //use count outside context: 4
pthread_join(threadID, NULL);
printf("use count after thread finished : %li n", (employee.use_count()));//use count after thread finished : 5
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
The thread is started within a special context, where the context is marked by the curly
braces { }. Within the context, the useCount
will have
the value 5 just before the thread is started. After the thread is started, the context
is exited while the thread sleeps for 5 seconds and in the meantime the Reference Count
is decreased to the value 4 by the Main Thread.
Once the thread wakes up again, it will make a local and a global copy of the shared pointer, resulting in the output of the value 6.
After the thread is terminated, only the global copy remains, resulting in an output of the value 5. The final result after exiting the visitCoffeeHouse method for the "Peter Parker" employee is the value 4.
At first glance, everything looks relatively normal with the threads. That is, the reference counts seem to be increased and decreased correctly. However, problems can never be completely ruled out. If you want to be completely sure, it is recommended to work only on a copy of the data.
The concept of C++ shared_ptr provides an alternative to traditional C++ pointers. However, it is crucial never to mix normal C++ pointers and shared pointers because this can lead to serious problems.
The great advantage of shared pointers, the automatic management of memory references, also replaces the tedious manual memory sharing. Furthermore, an insight was given into how reference counting behaves. Simply said: Whenever a new context is entered e.g. a method or a copy of a Shared Pointer, the Reference Count is increased by +1. As soon as a context is left again, the reference count is automatically reduced, depending on the number of copies in this context. After intensive familiarization with the topic, it can be stated that shared pointers can make the development process significantly more convenient and easier. They also make collaboration with team colleagues much easier. It should be exciting to use shared pointers in larger projects in the future.
However, there could be a small disadvantage: if you use shared pointers in a project and also want to use an external library such as OpenCV in the same project, which may not offer shared pointers in the interfaces, you should be very careful with it. It might be a good idea to use a "deep copy" of the data that you want to pass as parameters to the interfaces of the library.