Examining Smart Pointers

Memory management is crucial to designing reliable and efficient software. Mismanaging memory can lead to buffer overflows, segmentation faults, and memory leaks. Sometimes, mismanaging memory can be gravely detrimental, leading to exploitable vulnerabilities that malicious actors can leverage as attack surfaces.

Pointers are variables that are used for tracking the memory address of some other object such as an array or a structure. The pointer type that most programmers are familiar with is known as the raw pointer which is very prevalent in C and is sometimes used in C++. Below is an example of the typical use of a raw pointer in C.

pointers.c
C
#include <stdio.h>
int main() {
// Declare an integer
int x = 5;
// Create raw pointer to 'x'
int* ptr = &x;
// Print the value of 'ptr' which is the
// memory address of 'x'
printf("%p", ptr);
return 0;
}

Building and running the above code produces this output:

~ gcc pointers.c -o pointers
~ ./pointers
0x7ffda8c9049c

The text printed to the terminal is a hexadecimal string that represents the memory address of the data assigned to x. This is a pretty rudimentary use case for raw pointers; let’s look at something more complex.

Using malloc in C, we can manually allocate a slice of memory and capture a pointer to the starting address of that memory block.

alloc.c
C
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = (int*)malloc(sizeof(int));
printf("Allocated memory for an integer\n");
// Memory allocated with 'malloc' must be manually freed
// to prevent memory leaks
free(ptr);
printf("Freed the allocated memory");
// Pointers to deleted memory must be assigned the 'NULL'
// value to be properly cleaned up
ptr = NULL;
return 0;
}

In C, the responsibility of managing memory lies with the developer. If a block of memory is manually allocated with malloc(), calloc(), or realloc(), the developer must call free() to deallocate the memory. If the memory is not explicitly deallocated, a memory leak occurs. The same principle applies to memory allocated with the new keyword, except the memory must be deallocated with the delete keyword to prevent a leak.

unique.cpp
C++
#include <cstdint>
#include <iostream>
#include <memory>
struct CustomDeleter {
void operator()(uint64_t* i) {
std::cout << "Custom deleter called; pointer fell out of scope"
<< std::endl;
delete i;
}
};
int main() {
// Standard use of unique ptr
auto ptr = std::make_unique<uint64_t>(10);
std::cout << "Value at " << ptr.get() << ": " << *ptr << std::endl;
// Demonstration for destruction after falling out of scope
{
using custom_ptr_t = std::unique_ptr<uint64_t, CustomDeleter>;
custom_ptr_t ptr_with_deleter(new uint64_t(10));
}
std::cout << "Terminating";
return 0;
}

The above code demonstrates the standard use of a unique pointer as well as a showcase of how the unique pointer cleans up its owned memory whenever it falls out of scope. A custom deleter functor called CustomDeleter is declared and passed as a template parameter to the unique pointer type. A variable named ptr_with_deleter is then instantiated inside a local scope, and then it immediately falls out of scope. The custom deleter is invoked, and we get the following output when building and running:

~ g++ unique.cpp -o unique
~ ./unique
Value at 0x5fa1229862b0: 10
Custom deleter called; pointer fell out of scope
Terminating

Shared pointers are similar to unique pointers in that they also wrap a raw pointer and own the memory of that object; however, the difference is that multiple instances of a shared pointer can own a single instance of memory. Another crucial difference is that shared pointers have a reference counter. The reference counter represents how many instances of a shared pointer claim ownership of the same memory location. For memory owned by a shared pointer to be deallocated when the shared pointer falls out of scope, the reference counter must also be equal to 0. For example, if two shared pointers claim ownership of the same memory, and one falls out of scope, the memory is not deallocated because there still exists a shared pointer that claims ownership. Only when that shared pointer falls out of scope (which decrements the reference count to 0) will the memory be deallocated. Let’s look at an example implementation:

shared.cpp
C++
#include <cstdint>
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<uint64_t> ptr(new uint64_t(10));
std::shared_ptr<uint64_t> ptr2 = ptr;
std::cout << "Value at first pointer ("
<< ptr.get() << "): " << *ptr << std::endl;
std::cout << "Value at second pointer ("
<< ptr2.get() << "): " << *ptr2 << std::endl;
std::cout << "Reference count: " << ptr.use_count() << std::endl;
{
std::shared_ptr<uint64_t> ptr3 = ptr;
std::cout << "Reference count: " << ptr.use_count() << std::endl;
}
std::cout << "Reference count: " << ptr.use_count() << std::endl;
return 0;
}

The above code demonstrates that multiple instances of a shared pointer can “own” the same memory, and the output proves this. Compiling and running the code produces the following:

~ g++ shared.cpp -o shared
~ ./shared
Value at first pointer (0x5816b72602b0): 10
Value at second pointer (0x5816b72602b0): 10
Reference count: 2
Reference count: 3
Reference count: 2

Notice that ptr and ptr2 refer to the same memory address. They both maintain ownership of the memory that was originally initialized by ptr. Also notice that the reference count is 2 before ptr3 is initialized in its local scope; after it is initialized, the reference count increases to 3. Once ptr3 falls out of scope, the reference count drops back to 2.

The average C++ developer will be very familiar with unique and shared pointers, but the weak pointer may seem foreign and esoteric. In fact, doing a simple code search on GitHub for “std::weak_ptr” shows at the time of writing only 175k results, whereas “std::shared_ptr” and “std::unique_ptr” both yield over 4 million hits. It would seem that weak pointers are not very commonly used; what, then, is their purpose?

weak.cpp
C++
#include <cstdint>
#include <iostream>
#include <memory>
std::weak_ptr<uint64_t> weak;
void CheckPointer() {
std::cout << "Reference count: " << weak.use_count() << std::endl;
// Promote to shared pointer if weak pointer is valid
if (std::shared_ptr<uint64_t> shared_copy = weak.lock()) {
std::cout << "Value of weak pointer is "
<< *shared_copy << std::endl;
} else {
std::cout << "weak pointer is expired" << std::endl;
}
}
int main() {
{
auto shared = std::make_shared<uint64_t>(10);
weak = shared;
CheckPointer();
}
CheckPointer();
return 0;
}

In the CheckPointer function, notice the condition within the if statement: a temporary shared pointer is created if the weak pointer is valid. If the new shared pointer refers to valid memory, then the reference can be used. If the new shared pointer is invalid, we consider the weak pointer to have expired, so we cannot use the reference. A weak pointer is invalidated if the shared pointer from which the weak pointer was derived has been deallocated due to falling out of scope with a reference count of 0.

Building and running the above code produces the following output:

~ g++ weak.cpp -o weak
~ ./weak
Reference count: 1
Value of weak pointer is 10
Reference count: 0
weak pointer is expired

Notice that, even with the weak pointer being derived from the shared pointer, the weak pointer indicates a reference count of 1. This is because the weak pointer does not have ownership of the reference it received from the shared pointer as mentioned previously. Once the original shared pointer falls out of scope, the weak pointer is checked again, and this time, it is found to have expired.

So, what happens if we try to dereference a weak pointer without promoting it? Well, this is a compiler error. Let’s try to build the following code:

deref_weak.cpp
C++
#include <cstdint>
#include <memory>
int main() {
auto shared = std::make_shared<uint64_t>(10);
std::weak_ptr<uint64_t> weak = shared;
auto val = *weak; // Will not compile
return 0;
}
~ g++ deref_weak.cpp -o deref_weak
deref_weak.cpp: In function ‘int main()’:
deref_weak.cpp:9:14: error: no match for ‘operator*’ (operand type is ‘std::weak_ptr<long unsigned int>’)
9 | auto val = *weak; // Will not compile
| ^~~~~

It simply is not possible. Weak pointers cannot be dereferenced on their own. They must be promoted to a shared pointer before dereferencing can occur.

This naturally brings about the question: Why use weak pointers at all if they have to be promoted to a shared pointer to be useful? Weak pointers actually solve an interesting problem that can occur with shared pointers. Sometimes, a developer might implement their software design in such a way that two or more shared pointers end up referring to each other, forming a reference cycle. For instance:

cycle.cpp
C++
struct B;
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::shared_ptr<A> a_ptr;
};

A and B each have a shared pointer referring to one another, meaning that their reference count is now circular. Even when they fall out of scope, the reference count will not hit 0, meaning the memory is not deallocated, leading to a memory leak. This is where the weak pointer comes in.

fixed_cycle.cpp
C++
struct B;
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::weak_ptr<A> a_ptr;
};

The weak pointer does not increase the reference count of A, meaning that once both objects go out of scope, the memory will be safely and correctly deallocated.

The other use case for weak pointers is for checking whether an object exists without extending its lifetime. This is similar to what we implemented in the original weak pointer example. By using the lock() method to promote a weak pointer to a temporary shared pointer, we are able to determine if the object still exists; if it exists, our shared pointer will be valid.

Smart pointers are a major improvement upon raw pointers, implementing a quasi garbage collection system in C++. Where possible, smart pointers should be preferred over raw pointers. Understanding when, where, and how to use smart pointers is crucial to being a C++ developer so that you can produce efficient code with heightened memory safety.

  1. ISO 14882:2011 20.7.1 ↩︎
  2. std::unique_ptr – cppreference.com. (2025). Cppreference.com. https://en.cppreference.com/w/cpp/memory/unique_ptr.html ↩︎
  3. std::weak_ptr – cppreference.com. (2025). Cppreference.com. https://en.cppreference.com/w/cpp/memory/weak_ptr.html ↩︎

Discover more from shared_ptr

Subscribe now to keep reading and get access to the full archive.

Continue reading