πŸ““Smart Pointer in C++ 2023

Smart pointers are a useful C++ feature to manage memory and avoid memory leaks.

  • unique_ptr: Represents exclusive ownership of a resource. The resource is destroyed when the unique_ptr goes out of scope.

  • shared_ptr: Represents shared ownership of a resource. The resource is destroyed when the last shared_ptr goes out of scope.

  • weak_ptr: Represents a "weak" reference to a resource managed by a shared_ptr. A weak_ptr does not prolong the lifetime of the resource.

unique_ptr:

  • Stores a unique pointer to an object

  • Object is deleted when unique_ptr goes out of scope or is reset()

  • Example:

std::unique_ptr<int> uptr = std::make_unique<int>(10);
int * raw = uptr.get();  // Get raw pointer
uptr.reset();  // Delete object
  • Methods: get(), reset(), release(), operator->(), operator*()

shared_ptr:

  • Stores a shared pointer to an object

  • Object is deleted when all shared_ptrs are destroyed or reset

  • Example:

std::shared_ptr<int> sptr1 = std::make_shared<int>(10);
std::shared_ptr<int> sptr2 = sptr1;

sptr1.reset();  // sptr2 is still valid
sptr2.reset();  // Object deleted 
  • Methods: get(), reset(), use_count(), operator->(), operator*()

weak_ptr:

  • Stores a weak pointer to an object managed by shared_ptr

  • Does not increase the object's reference count

  • Example:

std::shared_ptr<int> sptr = std::make_shared<int>(10);
std::weak_ptr<int> wptr = sptr;

if (std::shared_ptr<int> ptr = wptr.lock()) { 
   // Use ptr 
}
  • Methods: lock(), expired()

Hope this overview of the main smart pointer types in C++ is helpful!

All the methods of smart pointer

Here are the main methods for each smart pointer type in C++:

unique_ptr:

  • get() - Returns the underlying raw pointer

  • reset() - Resets the unique pointer, deletes the object

  • release() - Releases ownership of the object, returns the raw pointer

  • operator->() - Overloaded dereference operator

  • operator*() - Overloaded dereference operator

shared_ptr:

  • get() - Returns the underlying raw pointer

  • reset() - Resets the shared pointer, may delete the object if no other shared pointers exist

  • use_count() - Returns the number of shared pointers referring to the object

  • operator->() - Overloaded dereference operator

  • operator*() - Overloaded dereference operator

weak_ptr:

  • lock() - Attempts to lock the weak pointer, returning a shared pointer if the object still exists

  • expired() - Checks if the object managed by the weak pointer still exists

Some other common methods:

  • make_unique() - Creates a unique pointer

  • make_shared() - Creates a shared pointer

  • static_pointer_cast() - Casts between different pointer types

  • dynamic_pointer_cast() - Uses RTTI to cast between different pointer types

  • const_pointer_cast() - Casts away constness

  • alias() - Creates an alias shared pointer (shares ownership)

Hope this overview of the main methods for each smart pointer type in C++ is helpful!

get()

get() for unique_ptr:

// Declare unique_ptr    
std::unique_ptr<int> up = std::make_unique<int>(10);

// Get raw pointer  
int* x = up.get();

// Use raw pointer   
std::cout << *x;  // Prints 10

get() for shared_ptr:

// Declare shared_ptr
std::shared_ptr<int> sp = std::make_shared<int>(10);

// Get raw pointer
int* y = sp.get();

// Use raw pointer
std::cout << *y; // Prints 10

The get() method allows you to access the underlying raw pointer stored by the smart pointer. You can then dereference that pointer to access the object.

Keep in mind that after calling get(), the smart pointer still owns the object and will delete it when it goes out of scope. The raw pointer returned by get() is only valid as long as the smart pointer exists.

The get() method provides a way to interoperate with code that uses raw pointers.

Hope this helps explain get()! Let me know if you have any other questions.

reset()

Calling reset() on a unique_ptr will:

// Declare unique_ptr    
std::unique_ptr<int> up = std::make_unique<int>(10);

// Reset the unique pointer  
up.reset();  

// up no longer owns the memory   
int* x = up.get();  // x is a dangling pointer!
  • Delete the object it was pointing to (using delete)

  • Release ownership of that memory

  • Make the unique_ptr empty (no longer owning any memory)

This essentially "resets" the unique pointer, allowing you to call make_unique() again to allocate new memory.

For shared_ptr:

std::shared_ptr<int> sp = std::make_shared<int>(10);

sp.reset();  

Calling reset() on a shared_ptr will:

  • Decrement the reference count

  • If the reference count reaches 0, the object will be deleted

  • Makes the shared_ptr empty, no longer owning any memory

So reset() allows you to essentially "reuse" the shared_ptr for a new allocation.

Hope this helps explain reset()! Let me know if you have any other questions.

release()

For unique_ptr:

std::unique_ptr<int> up = std::make_unique<int>(10);

int* raw = up.release();

// up is now empty!
assert(up.get() == nullptr);

// raw owns the memory  
std::cout << *raw; // Prints 10

delete raw;  // Manually delete memory

up.release() will:

  • Return the raw pointer owned by the unique_ptr

  • Release ownership of that memory

  • Leave the unique_ptr empty (no longer owning any memory)

This gives you the raw pointer so you can manually manage the memory. You are then responsible for deleting it.

For shared_ptr:

shared_ptr does not have a release() method since it uses reference counting.

Hope this helps explain release()! Let me know if you have any other questions.

use_count()

use_count() for shared_ptr will return the number of shared_ptr objects that are managing the same memory.

For example:

std::shared_ptr<int> p1 = std::make_shared<int>(5);
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;

std::cout << p1.use_count(); // Prints 3

p2.reset(); 

std::cout << p1.use_count(); // Prints 2

Here:

  • p1, p2 and p3 all point to the same memory allocation

  • use_count() initially returns 3, as 3 shared pointers are managing that memory

  • When p2 is reset(), the count decrements to 2

use_count() allows you to determine how many shared pointers are currently managing an allocation. When the count reaches 0, the memory will be deallocated.

use_count() is only available for shared_ptr since it uses reference counting.

Hope this helps explain use_count()! Let me know if you have any other questions.

lock()

For weak_ptr:

std::shared_ptr<int> p = std::make_shared<int>(5);

std::weak_ptr<int> wp = p;

// Check if memory still allocated
if(auto sp = wp.lock()) {
   std::cout << *sp; // Prints 5
} 

wp.lock() will:

  • Attempt to "lock" the weak pointer by converting it to a shared pointer

  • Only succeeds if the memory is still allocated (the shared_ptr count is > 0)

  • Returns an empty shared_ptr if the memory has been deallocated

This allows you to check if the memory referenced by the weak pointer still exists, by locking it into a shared pointer.

lock() returns a shared pointer (scoped into sp in the example). You can then use that shared pointer to access the memory.

lock() is only available for weak_ptr since it does not own the memory itself.

Hope this helps explain lock()! Let me know if you have any other questions.

expired()

For weak_ptr:

std::shared_ptr<int> p = std::make_shared<int>(5);

std::weak_ptr<int> wp = p;

// Check if memory has been deallocated 
if (wp.expired()) {
  std::cout << "Memory has expired!";
}

expired() will simply return true if the memory referenced by the weak pointer has been deallocated. It will return false if the memory is still alive.

You can use expired() to check if a weak pointer is still valid before attempting to lock() it.

For example:

if (!wp.expired()) {
  // Memory still alive, try to lock weak pointer  
  std::shared_ptr<int> sp = wp.lock();
}

expired() is only available for weak_ptr since it does not own the memory itself. It must check the state of the corresponding shared_ptr.

Hope this helps explain expired()! Let me know if you have any other questions.

operator->() or operator*() - Overloaded dereference operator

The operator->() and operator*() methods are overloaded dereference operators for smart pointers. They allow you to access members and dereference the smart pointer similar to a raw pointer.

For example, with a unique_ptr:

std::unique_ptr<Foo> up = std::make_unique<Foo>();

up->someMethod();   // Call method using operator->

(*up).someMethod(); // Call method using operator*

Here we are able to call a method on the Foo object using either:

  • operator->() , which lets us use the -> access operator

  • operator*(), which lets us dereference the pointer using *

This allows smart pointers to be used in a similar way to raw pointers.

The overloaded dereference operators are implemented for:

  • unique_ptr

  • shared_ptr

  • weak_ptr

So in summary, operator->() and operator*() allow smart pointers to be used similarly to raw pointers, by overloading the pointer dereference syntax.

Hope this explanation helps! Let me know if you have any other questions regarding smart pointers.

make_unique() - Creates a unique pointer

make_unique() allows you to easily create a unique_ptr with one call, rather than manually allocating memory and constructing a unique_ptr.

For example:

std::unique_ptr<Foo> up = std::make_unique<Foo>(10); 

Here std::make_unique<Foo>(10) will:

  • Dynamically allocate memory for a Foo object

  • Call the Foo constructor with the value 10

  • Construct a unique_ptr managing that memory

All in one call.

Compared to:

Foo* raw = new Foo(10);
std::unique_ptr<Foo> up(raw);

make_unique() provides a cleaner syntax to create unique pointers.

It takes a template argument for the pointer type, and arguments to forward to the object's constructor.

Hope this helps explain make_unique()! Let me know if you have any other questions.

make_shared() - Creates a shared pointer

make_shared() works similarly to make_unique() but for shared_ptr instead.

It allows you to create a shared_ptr in one call, rather than manually allocating memory and constructing the shared_ptr.

For example:

std::shared_ptr<Foo> sp = std::make_shared<Foo>(10);

This will:

  • Dynamically allocate memory for a Foo object

  • Call the Foo constructor with 10

  • Construct a shared_ptr managing that memory

All in one call.

Compared to:

Foo* raw = new Foo(10);  
std::shared_ptr<Foo> sp(raw);

make_shared() is generally preferred over manually allocating memory and constructing the shared_ptr because:

  • It allows the memory allocation and shared_ptr construction to be atomic

  • It may allocate the reference counter block alongside the object, reducing memory usage

Hope this helps explain make_shared()! Let me know if you have any other questions.

static_pointer_cast() - Casts between different pointer types

static_pointer_cast allows you to cast between different pointer types, similar to a static_cast.

For example:

class Base {};
class Derived: public Base {};

std::unique_ptr<Base> base = std::make_unique<Derived>();

std::unique_ptr<Derived> derived = 
    std::static_pointer_cast<Derived>(base);

Here we:

  • Create a unique_ptr to a Base class

  • It actually points to a Derived object

  • We cast it to a unique_ptr to Derived using static_pointer_cast

This performs a safe downcast from Base to Derived, since we know the actual type is Derived.

static_pointer_cast uses the compile time type information to perform the cast. It is similar to a static_cast.

Advantages over a dynamic_pointer_cast :

  • Faster - No RTTI is used

  • Safer - Compiler can check the cast is valid

static_pointer_cast allows you to cast between compatible smart pointer types where you know the actual type at compile time.

Hope this helps explain static_pointer_cast! Let me know if you have any other questions.

dynamic_pointer_cast() - Uses RTTI to cast between different pointer types

dynamic_pointer_cast uses RTTI (run-time type information) to perform a cast between compatible smart pointer types.

For example:

class Base {};
class Derived: public Base {};

std::unique_ptr<Base> base = std::make_unique<Derived>();

std::unique_ptr<Derived> derived =
    std::dynamic_pointer_cast<Derived>(base);

Here we:

  • Create a unique_ptr to a Base class

  • It actually points to a Derived object

  • We cast it to a unique_ptr to Derived using dynamic_pointer_cast

This performs a downcast from Base to Derived, using RTTI to determine the actual object type at run-time.

Advantages over static_pointer_cast:

  • Can cast to pointers of unrelated types, as long as a conversion exists

  • Will return a null pointer if the cast is not possible

dynamic_pointer_cast uses RTTI to determine if a cast is possible, and performs it at run-time.

It allows you to cast between smart pointer types when you don't know the exact types at compile time.

Hope this helps explain dynamic_pointer_cast! Let me know if you have any other questions.

const_pointer_cast() - Casts away constness

const_pointer_cast allows you to cast away the const qualifier from a smart pointer, effectively casting away constness.

For example:

std::shared_ptr<const int> cptr = std::make_shared<const int>(5);

std::shared_ptr<int> ptr = 
    std::const_pointer_cast<int>(cptr);

*ptr = 10;  // Ok, we cast away const 

Here we:

  • Create a shared pointer to a const int

  • Cast it to a shared pointer to a non-const int using const_pointer_cast

  • We are then able to modify the value, since we cast away const

const_pointer_cast effectively performs a const_cast on the underlying pointer.

It should be used with caution, as casting away constness can easily lead to undefined behavior.

const_pointer_cast allows you to cast smart pointers while casting away the const qualifier, but it is not recommended in most cases. Prefer avoiding the need for such casts where possible.

Hope this helps explain const_pointer_cast! Let me know if you have any other questions.

alias() - Creates an alias shared pointer (shares ownership) explain one by one

alias() for shared_ptr allows you to create an alias shared pointer that refers to the same object and shares ownership.

For example:

std::shared_ptr<int> p1 = std::make_shared<int>(5);

std::shared_ptr<int> p2 = p1.alias();

Here:

  • We create a shared pointer p1 pointing to an integer

  • We call alias() on p1 to create an alias shared pointer p2

After this, both p1 and p2:

  • Point to the same integer object

  • Share ownership of that object

  • Have the same reference count

When either p1 or p2 is destroyed or reset, the other will still be valid since they share ownership.

alias() allows you to easily create a second shared pointer that refers to the same object, without copying the reference count.

The new alias shared pointer's reference count is tied to the original shared pointer.

Hope this helps explain alias()! Let me know if you have any other questions.

Last updated