Skip to content
Snippets Groups Projects
Commit e7482ad1 authored by Marcus Mohr's avatar Marcus Mohr
Browse files

Start notebook #15 on smart pointers

We begin by implementing a homebrew class ResourcePointer as example
parent 50740005
No related branches found
No related tags found
No related merge requests found
%% Cell type:markdown id:cf0bbc25-707e-4d3a-88b2-5a2559000469 tags:
# Smart Pointers
%% Cell type:markdown id:68ec4466-5b37-4c08-b2a0-e2729b0cd0eb tags:
A very helpful addition to C++ was the introduction of so called **smart pointers** in C++11. These are the template classes:
- `std::shared_ptr`
- `std::unique_ptr`
- `std::weak_ptr`
---
Conceptually an object that dynamically allocates some kind of resource should also take care of deallocating that resource again. However, if the object needs to allow access to that address from the outside for some reason, it get's difficult. A common situation is that multiple objects, not necessarily of the same class, want to share a resource. In that situation an object must not deallocate it, if there are still others around that use it.
As a motivation we are going to try to implement a *wrapper class* for a pointer to some shared resource which handles the 'reference counting' and performs the deallocation. The resource
will be passed to the constructor from the outside. So the specification would be something like:
- constructor accepts pointer to resource
- objects should be copyable to allow sharing of resource
- wrapper class keeps track on how many objects sharing the same resource (i.e. holding the same pointer) exist
- when an object is destroyed it should deallocate the resource, if it is the last one holding it
%% Cell type:markdown id:a04f1904-d672-41c6-9d55-5c3c66323947 tags:
**Brainstorming:**<br>
How can we implement this? What ingredients should we use?
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
%% Cell type:markdown id:8806369d-2415-435d-9649-302478522064 tags:
Our wrapper class should allow to generate objects holding resources of different type. One way to accomplish this is to use a template class.
%% Cell type:code id:e5fac784-75bb-4fa8-9561-0c57d24f6a31 tags:
``` C++17
template< typename T >
class ResourcePointer {
private:
T* rawPtr_ = nullptr;
};
```
%% Cell type:markdown id:657d6dd0-e73c-4b93-a5d8-ad6daeec11fc tags:
- We want to allocate the resource outside of our wrapper and initialise it with its address.
- Hence we should not allow the default c'tor
- In order to avoid problems we will nullify the pointer variable we receive.
%% Cell type:code id:c3d46bb1-c839-4655-9d65-31485c63969a tags:
``` C++17
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// resource now belongs to us!
ptr = nullptr;
}
private:
T* rawPtr_ = nullptr;
};
```
%% Cell type:markdown id:a937bd6e-1f2c-4ae2-b84b-5f233ccae8fc tags:
As the constructed object holds the ressource's address now, it should increase the reference count!
Counting over multiple objects of the same class can be accomplished with a static member (class variable). However, one member will not be sufficient, since we need to distinguish groups of objects with different ressources.
A map can help us here. Since a pointer stores a memory address, which is a form of integer, we can directly use that as a **key**. The associated **value** will be our reference count.
%% Cell type:code id:2b18526d-d8c9-4b3d-843f-3594a8976676 tags:
``` C++17
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// resource now belongs to us!
ptr = nullptr;
}
private:
T* rawPtr_ = nullptr;
static std::map< T*, int > ptrCounts_;
};
```
%% Cell type:markdown id:18481050-4e02-4f20-928b-4aaf4992371f tags:
We need to initialise the static member outside of the class, due to its type.
%% Cell type:code id:fa335307-a14b-4550-8f6c-a2fd12ed9130 tags:
``` C++17
template< typename T >
std::map< T*, int > ResourcePointer<T>::ptrCounts_ = std::map< T*, int >();
```
%% Cell type:markdown id:cd36b937-050e-4ec8-b44c-e9beb5669fb0 tags:
Now we can update our constructor:
%% Cell type:code id:76634dc4-9760-48af-9b0c-df8e55f4e07b tags:
``` C++17
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// while this should not happen, better check whether there
// are already objects that hold this resource
if( ptrCounts_.find( rawPtr_ ) != ptrCounts_.end() ) {
// increment reference counter
ptrCounts_[ rawPtr_ ]++;
}
else {
// insert (key,value) pair into map
ptrCounts_.insert( { rawPtr_, 1 } );
}
// resource now belongs to us!
ptr = nullptr;
}
private:
T* rawPtr_ = nullptr;
static std::map< T*, int > ptrCounts_;
};
```
%% Cell type:markdown id:99d41dbc-0f9f-46e7-a212-0e7c6c6c5e15 tags:
Next step is to implement the destructor. It should deallocate the resource, if and only if, the object being destroyed is the last one owning it.
%% Cell type:code id:6648676d-e964-414d-9aa6-58319f64abfc tags:
``` C++17
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// while this should not happen, better check whether there
// are already objects that hold this resource
if( ptrCounts_.find( rawPtr_ ) != ptrCounts_.end() ) {
// increment reference counter
ptrCounts_[ rawPtr_ ]++;
}
else {
// insert (key,value) pair into map
ptrCounts_.insert( { rawPtr_, 1 } );
}
// resource now belongs to us!
ptr = nullptr;
}
~ResourcePointer() {
// check whether we still own the ressource
if( rawPtr_ != nullptr ) {
// delete resource, if we are the last one
int count = ptrCounts_.at( rawPtr_ ) - 1;
if( count == 0 ) {
// object pointer or array pointer?
if( std::is_array_v< decltype( rawPtr_ ) > ) {
delete[] rawPtr_;
}
else {
delete rawPtr_;
}
}
// update reference count
ptrCounts_.at( rawPtr_ ) = count;
}
}
private:
T* rawPtr_ = nullptr;
static std::map< T*, int > ptrCounts_;
};
```
%% Cell type:markdown id:7404da4f-7e7b-4fc5-8ffc-66d612601c69 tags:
Okay, the foundations are laid. However, to allow sharing we now implement a copy constructor and copy assignment operator
%% Cell type:code id:4951abc1-158c-413f-9e14-e889df36571f tags:
``` C++17
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// while this should not happen, better check whether there
// are already objects that hold this resource
if( ptrCounts_.find( rawPtr_ ) != ptrCounts_.end() ) {
// increment reference counter
ptrCounts_[ rawPtr_ ]++;
}
else {
// insert (key,value) pair into map
ptrCounts_.insert( { rawPtr_, 1 } );
}
// resource now belongs to us!
ptr = nullptr;
}
// copy constructor
ResourcePointer( const ResourcePointer< T >& other ) {
rawPtr_ = other.rawPtr_;
ptrCounts_.at( rawPtr_ )++;
}
// copy assignment
ResourcePointer< T >& operator=( const ResourcePointer< T >& other ) {
ResourcePointer< T > newPtr;
newPtr.rawPtr_ = other.rawPtr_;
ptrCounts_.at( rawPtr_ )++;
return newPtr;
}
~ResourcePointer() {
// check whether we still own the ressource
if( rawPtr_ != nullptr ) {
// delete resource, if we are the last one
int count = ptrCounts_.at( rawPtr_ ) - 1;
if( count == 0 ) {
// object pointer or array pointer?
if( std::is_array_v< decltype( rawPtr_ ) > ) {
delete[] rawPtr_;
}
else {
delete rawPtr_;
}
}
// update reference count
ptrCounts_.at( rawPtr_ ) = count;
}
}
private:
T* rawPtr_ = nullptr;
static std::map< T*, int > ptrCounts_;
};
```
%% Cell type:markdown id:d2abd86f-c6c2-416a-98f5-16e502360e4f tags:
**What's missing?**<br><br>
Well, we implemented
- destructor
- copy constructor
- copy assignment operator
Thus, following the **rule-of-five/six**, we should also implement
- move constructor
- move assignment operator
as we already deleted the default constructor.
%% Cell type:code id:c18a33a8-0444-4fb6-86e3-f8db5703e3c6 tags:
``` C++17
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// while this should not happen, better check whether there
// are already objects that hold this resource
if( ptrCounts_.find( rawPtr_ ) != ptrCounts_.end() ) {
// increment reference counter
ptrCounts_[ rawPtr_ ]++;
}
else {
// insert (key,value) pair into map
ptrCounts_.insert( { rawPtr_, 1 } );
}
// resource now belongs to us!
ptr = nullptr;
}
// copy constructor
ResourcePointer( const ResourcePointer< T >& other ) {
rawPtr_ = other.rawPtr_;
ptrCounts_.at( rawPtr_ )++;
}
// copy assignment
ResourcePointer< T >& operator=( const ResourcePointer< T >& other ) {
ResourcePointer< T > newPtr;
newPtr.rawPtr_ = other.rawPtr_;
ptrCounts_.at( rawPtr_ )++;
return newPtr;
}
// move constructor
ResourcePointer( ResourcePointer< T >&& other ) {
rawPtr_ = other.rawPtr_;
other.rawPtr_ = nullptr;
};
// move assignment
ResourcePointer< T >& operator=( ResourcePointer< T >&& other ) {
ResourcePointer< T > newPtr;
newPtr.rawPtr_ = other.rawPtr_;
other.rawPtr_ = nullptr;
return newPtr;
};
// destructor
~ResourcePointer() {
// check whether we still own the ressource
if( rawPtr_ != nullptr ) {
// delete resource, if we are the last one
int count = ptrCounts_.at( rawPtr_ ) - 1;
if( count == 0 ) {
// object pointer or array pointer?
if( std::is_array_v< decltype( rawPtr_ ) > ) {
delete[] rawPtr_;
}
else {
delete rawPtr_;
}
}
// update reference count
ptrCounts_.at( rawPtr_ ) = count;
}
}
private:
T* rawPtr_ = nullptr;
static std::map< T*, int > ptrCounts_;
};
```
%% Output
Writing ResourcePointer.hpp
%% Cell type:markdown id:d395e36d-9235-467f-9fa5-72a72ab3a7ee tags:
Finally we add a getter method `getCount()` to showcase the class' functionality
%% Cell type:code id:4ccd404f-bb31-44ec-b8a5-91361bc4abf6 tags:
``` C++17
%%file ResourcePointer.hpp
template< typename T >
class ResourcePointer {
public:
ResourcePointer() = delete;
ResourcePointer( T*& ptr ) : rawPtr_( ptr ) {
// while this should not happen, better check whether there
// are already objects that hold this resource
if( ptrCounts_.find( rawPtr_ ) != ptrCounts_.end() ) {
// increment reference counter
ptrCounts_[ rawPtr_ ]++;
}
else {
// insert (key,value) pair into map
ptrCounts_.insert( { rawPtr_, 1 } );
}
// resource now belongs to us!
ptr = nullptr;
}
// copy constructor
ResourcePointer( const ResourcePointer< T >& other ) {
rawPtr_ = other.rawPtr_;
ptrCounts_.at( rawPtr_ )++;
}
// copy assignment
ResourcePointer< T >& operator=( const ResourcePointer< T >& other ) {
ResourcePointer< T > newPtr;
newPtr.rawPtr_ = other.rawPtr_;
ptrCounts_.at( rawPtr_ )++;
return newPtr;
}
~ResourcePointer() {
// check whether we still own the ressource
if( rawPtr_ != nullptr ) {
// delete resource, if we are the last one
int count = ptrCounts_.at( rawPtr_ ) - 1;
if( count == 0 ) {
// object pointer or array pointer?
if( std::is_array_v< decltype( rawPtr_ ) > ) {
delete[] rawPtr_;
}
else {
delete rawPtr_;
}
}
// update reference count
ptrCounts_.at( rawPtr_ ) = count;
}
}
int getCount() const {
return ptrCounts_.at( rawPtr_ );
}
private:
T* rawPtr_ = nullptr;
static std::map< T*, int > ptrCounts_;
};
template< typename T >
std::map< T*, int > ResourcePointer<T>::ptrCounts_ = std::map< T*, int >();
```
%% Output
Overwriting ResourcePointer.hpp
%% Cell type:markdown id:60213dfc-fd96-4a54-acf1-f550c3bdfa0c tags:
Now let us implement a driver to test our class
%% Cell type:code id:1b5bd85b-372a-4e04-bbfe-4c75b4920d28 tags:
``` C++17
%%file driver.cpp
#include <type_traits>
#include <iostream>
#include <map>
#include "ResourcePointer.hpp"
int main() {
double* array = new double[100];
std::cout << " array = " << array << std::endl;
ResourcePointer< double > ptr1( array );
std::cout << " array = " << array << std::endl;
ResourcePointer ptr2{ ptr1 };
std::cout << "Resource used by " << ptr1.getCount() << " objects"
<< std::endl;
for( int k = 0; k < 5; ++k ) {
ResourcePointer localPtr = ptr1;
std::cout << "k = " << k << ": Resource used by " << localPtr.getCount()
<< " objects" << std::endl;
}
std::cout << "Resource used by " << ptr1.getCount() << " objects"
<< std::endl;
ResourcePointer ptr3{ std::move( ptr2 ) };
std::cout << "Resource used by " << ptr1.getCount() << " objects"
<< std::endl;
std::string* strPtr = new std::string;
*strPtr = "Shared String";
ResourcePointer stringRes{ strPtr };
std::cout << "String Resource used by " << stringRes.getCount() << " objects"
<< std::endl;
}
```
%% Output
Overwriting driver.cpp
%% Cell type:code id:05933e8c-c764-4cf1-a2e3-02a541f3da95 tags:
``` C++17
!g++ -std=c++17 driver.cpp
```
%% Cell type:code id:ac21d9d3-4169-4e8e-a998-c307f2c52790 tags:
``` C++17
!./a.out
```
%% Output
array = 0x557ed1e53e70
array = 0
Resource used by 2 objects
k = 0: Resource used by 3 objects
k = 1: Resource used by 3 objects
k = 2: Resource used by 3 objects
k = 3: Resource used by 3 objects
k = 4: Resource used by 3 objects
Resource used by 2 objects
Resource used by 3 objects
String Resource used by 1 objects
%% Cell type:code id:c3c74078-5fb3-46df-9fb2-6a2fe2f12d2c tags:
``` C++17
```
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment