C++. Smart pointers. Pointer classes unique_ptr, shared_ptr, weak_ptr

Smart pointers. Pointer classes unique_ptr, shared_ptr, weak_ptr

Before studying this topic, it is recommended that you familiarize yourself with the following topic:


Contents


Search other resources:

1. Using smart pointers in C++

In modern C++, the Standard Library includes so-called “smart pointers”. These pointers are designed to safely (without exceptions) release of memory and avoid resource leaks.

Smart pointers are defined in the std namespace and in the <memory> header file.

The main purpose of using smart pointers is to ensure that the resource is obtained at the same time as the object is initialized in one line of code (rather than several, as was previously the case with so-called raw pointers).

In practice, it looks like this. A stack-allocated object takes ownership of a resource from the heap. This resource can be, for example, dynamically allocated memory, an object descriptor, and so on. The object that acquires ownership of the resource contains a destructor that implements code to remove or free the resource or clean it up.

If you want to initialize a raw (raw) pointer to a real resource address or resource handle, it is recommended that you pass that raw pointer to a smart pointer. To avoid invisible memory allocation/deallocation errors, it is recommended to use raw pointers in small scoped code blocks, loops, or helper functions. In small blocks of code, using raw pointers maximizes performance and reduces memory ownership confusion.

A similar idiom is implemented in C#, Java.

 

2. Types of smart pointers

The following are the kinds of smart pointers you should use to encapsulate plain old (raw) C++ pointers.

2.1. Pointer unique_ptr

The unique_ptr type pointer is declared in the std namespace. The pointer reference can be std::unique_ptr.

This pointer can encapsulate (possess) only one pointer of the old type. Typically, this pointer is used when there is no need to use shared_ptr pointers. The unique_ptr can be transferred (moved) to a new owner, but not copied or shared. It replaces the already deprecated auto_ptr. This pointer is small (one pointer) and maintains an rvalue reference.

This pointer is used to quickly insert and retrieve data from C++ collections.

In the most general case, the declaration of a unique_ptr looks like this:

unique_ptr<T> ptr(rawPtr);

here

  • ptr – pointer of unique_ptr type;
  • rawPtr – raw pointer to type T;
  • T is the type of the value pointed to by ptr and rawPtr pointers.

A pointer of type unique_ptr cannot be assigned to another pointer of type unique_ptr. This means that an assignment like

unique_ptr<int> p1(new int(25));
unique_ptr<int> p2;
p2 = p1; // forbidden, error

will cause a compile-time error. The same goes for the case of declaring a pointer and initializing it.

The unique_ptr type pointer has the following main methods:

  • get() – returns a standard pointer to an object based on the unique_ptr pointer without transferring ownership of that object. After the method is called, the unique_ptr pointer continues to point to the same object;
  • release() – returns a standard pointer based on the unique_ptr pointer with transfer of ownership. After the method is called, the value of the unique_ptr pointer is nullptr;
  • reset() – resets the ownership of the object. After the method is called, the value of the unique_ptr pointer is nullptr.

 

2.2. Pointer shared_ptr

The shared_ptr is implemented in the std namespace. Full access to the std::shared_ptr pointer. This is a smart pointer with reference counting. This pointer is used when one raw pointer needs to be assigned to multiple owners. For example, when returning a copy of an object from a container, provided that the original is preserved.

The shared_ptr pointer implements a count of the number of references to a resource. The resource is released only when the number of references to it is equal to 0. This is how the garbage collector works.

In the most general case, the creation of a pointer of type shared_ptr can be done in one of two ways:

shared_ptr<T> ptr;
shared_ptr<T> ptr(rawPtr);

here

  • ptr is the name of the shared_ptr type pointer to be created;
  • T is the data type the pointer points to;
  • rawPtr – standard pointer to data of type T (object of type T). This pointer serves as a source for obtaining a pointer ptr of type shared_ptr<T>.

The following methods are defined for the shared_ptr pointer:

  • get() – returns a standard pointer based on the shared_ptr pointer without transferring rights to the object. After the method is called, the shared_ptr pointer points to the same object as before the call;
  • swap() – swaps the values of two pointers;
  • reset() – resets the ownership of the object;
  • use_count() – returns the number of copies of object pointers. The standard pointers received by the get() method are also taken into account;
  • operator function operator=() that implements the assignment of pointers of the shared_ptr type.

 

2.3. Pointer weak_ptr

The weak_ptr pointer is implemented in the std namespace. Accordingly, the access to the pointer has the form std::weak_ptr.

A std::weak_ptr pointer is a smart pointer that contains a so-called “weak” reference to the object pointed to by a pointer of type shared_ptr. The term “weak” means that the weak_ptr pointer does not own the object it points to. The owner of this object is the shared_ptr pointer. The weak_ptr specifies temporary ownership, which is when an object should only be accessed when it is already pointed to by a shared_ptr that owns the object. At the same time, changing the value of the object using weak_ptr will not work. The value of an object can only be read.

In order to use the weak_ptr pointer to change the value of the object it points to, you need to convert it to a shared_ptr pointer using the lock() method. In this case, if the original shared_ptr is destroyed, then the lifetime of the object continues until the temporary shared_ptr that was obtained from the weak_ptr is destroyed.

The weak_ptr pointer is useful when you need to handle multiple references that cycle through objects managed by shared_ptr pointers. If there are no shared external pointers in such a reference cycle, then the shared_ptr counter cannot reach zero. As a result, a memory leak occurs. To avoid this situation, one of the pointers in the loop is set to weak_ptr.

In general, getting a pointer of type weak_ptr from a pointer of type shared_ptr looks like this

shared_ptr<T> pShared(pRaw);
weak_ptr<T> pWeak = pShared;

here

  • pRaw – a raw pointer pointing to data of type T;
  • pShared – a pointer of shared_ptr type;
  • pWeak – a pointer of weak_ptr type;
  • T – data type pointed to by pRaw, pShared, pWeak pointers.

The following methods are defined for the weak_ptr type pointer:

  • lock() – converts a pointer of weak_ptr type to a pointer of shared_ptr type;
  • swap() – swaps weak_ptr type pointers;
  • operator function operator=() that assigns weak_ptr type pointers.

 

3. An example demonstrating the use of unique_ptr pointer

The example shows:

  • creation of a pointer of type unique_ptr;
  • converting (copying) the unique_ptr pointer into a standard pointer using the get() function;
  • resetting the value of the unique_ptr pointer using the reset() function;
  • passing of rights to the standard (raw) pointer using the release() function.

 

#include <iostream>
using namespace std;

void main()
{
  // Pointer unique_ptr - replacement for auto_ptr, works like a classic pointer
  // 1. Declare a pointer of type unique_ptr pointing to the number 55
  unique_ptr<int> pI(new int(55));

  // 2. Get classic pointer with get() function
  int* p;
  p = pI.get(); // pointers p and pI point to the same memory area
  cout << "*p = " << *p << endl; // *p = 55
  cout << "*pI = " << *pI << endl; // *pI = 55

  // 3. Change the value at the pointer *p
  *pI = 88;
  cout << "*p = " << *p << endl; // *p = 88
  cout << "*pI = " << *pI << endl; // *pI = 88

  // 4. Reset ownership of the reset() function,
  // the memory allocated for the pointer is freed here
  pI.reset(); // value 88 is released
  cout << "*p = " << *p << endl; // *p = -572662307 - pointer does not point to 88, pI => nullptr
  if (pI == nullptr)
    cout << "pI == nullptr" << endl; // +
  else
    cout << "pI != nullptr" << endl;

  // 5. Demonstration of the release() function
  // 5.1. Declare unique_ptr pointer
  unique_ptr<int> pI2(new int(333)); // pI2 => 333

  // 5.2. Pass rights to pointer p2 with reset rights from pointer pI2
  int* p2 = pI2.release(); // p2 => 333, pI2 => nullptr - releases

  // 5.3. Print the value of pointers
  cout << "*p2 = " << *p2 << endl; // *p2 = 333

  if (pI2 == nullptr)
    cout << "pI2 == nullptr" << endl; // +
  else
    cout << "pI2 != nullptr " << endl;

  // 6. Attempt to pass rights to another unique_ptr
  //unique_ptr<int> pI3 = pI2; // compilation error, forbidden
}

Program result

*p = 55
*pI = 55
*p = 88
*pI = 88
*p = -572662307
pI == nullptr
*p2 = 333
pI2 == nullptr

 

4. An example demonstrating the use of shared_ptr pointer

The example demonstrates the use of the shared_ptr pointer.

#include <iostream>
using namespace std;

void main()
{
  // 1. Declare 2 pointers of type shared_ptr and fill them with data
  shared_ptr<float> pF1(new float(2.8f));
  shared_ptr<float> pF2(new float(7.55f));

  // 2. Declare a pointer pointing to pF1
  shared_ptr<float> pF3 = pF1; // pF1 not released, pF also indicates 2.8f

  // Now pointers pF3 and pF1 point to the same memory area,
  // which contains the number 2.8f

  // 3. Output all pointers
  cout << "pF1 => " << *pF1 << endl; // pF1 => 2.8
  cout << "pF2 => " << *pF2 << endl; // pF2 => 7.55
  cout << "pF3 => " << *pF3 << endl; // pF3 => 2.8

  // 4. Object 7.55 is released here
  pF2 = pF3;

  // 5. Change the value of pF2 and display the value of all pointers
  *pF2 = 8.99f;
  cout << "pF1 => " << *pF1 << endl; // pF1 => 8.99
  cout << "pF2 => " << *pF2 << endl; // pF2 => 8.99
  cout << "pF3 => " << *pF3 << endl; // pF3 => 8.99

  // 6. Pointer of type shared_ptr. Method swap()
  shared_ptr<int> p1(new int(33));
  shared_ptr<int> p2(new int(77));

  // Swap p1 <=> p2
  p1.swap(p2); // p1 => 77, p2 => 33
  cout << *p1 << endl; // 77
  cout << *p2 << endl; // 33

  // 7. Method get(). Convert to standard int*
  int* pI = p1.get();
  cout << *pI << endl; // 77
  cout << *p1 << endl;

  // 8. Method reset() - reset the ownership of the object
  p2.reset();
  if (p2 == nullptr)
    cout << "p2 == nullptr" << endl; // +
  else
    cout << "p2 != nullptr" << endl;

  // 9. Method use_count() - returns the number of copies of pointers to an object
  cout << p1.use_count() << endl; // 1
} // all pointers are released here

Program result

pF1 => 2.8
pF2 => 7.55
pF3 => 2.8
pF1 => 8.99
pF2 => 8.99
pF3 => 8.99
77
33
77
77
33
p2 == nullptr

 

5. An example demonstrating the use of weak_ptr pointer

The example shows:

  • creation of a weak_ptr pointer based on a shared_ptr type pointer;
  • converting a weak_ptr pointer into a shared_ptr pointer using the lock() method;
  • using the use_count() method to get the number of copies of pointers pointing to data;
  • the impossibility of access by value for the pointer weak_ptr.

 

#include <iostream>
using namespace std;

void main()
{
  // Pointer weak_ptr
  // 1. Declare pointer of shared_ptr type
  shared_ptr<int> pShared1(new int(25)); // pI1 => 25

  // 2. Declare a pointer of type weak_ptr that points to the number 25
  weak_ptr<int> pWeak = pShared1;
  cout << "*pI2.use_count() = " << pWeak.use_count() << endl; // pI2.use_count() = 1

  // 3. Convert weak_ptr pointer to shared_ptr
  shared_ptr<int> pShared2 = pWeak.lock();
  cout << "*pI2.use_count() = " << pWeak.use_count() << endl; // pI2.use_count() = 2

  // 4. Display value at pShared2 pointer
  cout << "*pShared2 = " << *pShared2 << endl;

  // 5. Displaying the value at the pWeak pointer will not work
  // cout << *pWeak << endl; - compilation error

  // 6. Method swap() - swap weak_ptr pointers
  weak_ptr<int> pw1;
  weak_ptr<int> pw2;
  shared_ptr<int> ps1(new int(38));
  shared_ptr<int> ps2(new int(55));
  pw1 = ps1; // pw1 => 38
  pw2 = ps2; // pw2 => 55

  pw1.swap(pw2);

  cout << "pw1 => " << *pw1.lock().get() << endl; // pw1 => 55
  cout << "pw2 => " << *pw2.lock().get() << endl; // pw2 => 38
}

Program result

*pI2.use_count() = 1
*pI2.use_count() = 2
*pShared2 = 25
pw1 => 55
pw2 => 38

 


Related topics