Smart Pointers in C++

Smart pointers are an important feature introduced in C++11 and later versions, used for automatic management of dynamically allocated memory, avoiding memory leaks and dangling pointer issues. Smart pointers utilize RAII (Resource Acquisition Is Initialization) technique to automatically release the managed resources when the object’s lifecycle ends. Below is a detailed introduction to smart pointers in C++:

1. Why Do We Need Smart Pointers?

Traditional manual memory management in C++ (using new and delete) has the following issues:

  • Memory Leak: Forgetting to call delete.

  • Dangling Pointer: The object pointed to by the pointer has been released, but it is still in use.

  • Exception Safety: delete may not be executed when an exception occurs.

Smart pointers solve these problems by automatically releasing memory.

2. Types of Smart Pointers in C++

The C++ standard library provides three main types of smart pointers:

  • std::unique_ptr

  • std::shared_ptr

  • std::weak_ptr

3. std::unique_ptr

Characteristics

  • Exclusive Ownership: Only one unique_ptr can point to the object at any time.

  • Lightweight: Almost no additional overhead, performance is close to raw pointers.

  • Non-Copyable: But ownership can be transferred using std::move.

Example

#include <memory>
void unique_ptr_example() {
    // Creation method 1: direct initialization
    std::unique_ptr<int> ptr1(new int(42));
    // Creation method 2 (recommended): use make_unique (C++14 and later)
    auto ptr2 = std::make_unique<int>(42);
    // Transfer ownership
    std::unique_ptr<int> ptr3 = std::move(ptr1);  // ptr1 is now null
    // Access object
    std::cout << *ptr3 << std::endl;  // Output: 42
    // Custom deleter (optional)
    auto deleter = [](int* p) {
        std::cout << "Custom deleting..." << std::endl;
        delete p;
    };
    std::unique_ptr<int, decltype(deleter)> ptr4(new int(100), deleter);
}  // ptr3 and ptr4 automatically release memory when they go out of scope

Application Scenarios

Managing dynamically allocated resources (such as file handles, network connections).

As container elements (e.g., std::vector<std::unique_ptr<Base>> to implement polymorphic containers).

4. std::shared_ptr

Characteristics

  • Shared Ownership: Manages multiple pointers pointing to the same object through reference counting.

  • Thread-Safe: Incrementing and decrementing the reference count is atomic.

  • Higher Overhead: Each shared_ptr maintains a reference count and weak reference count.

Example

#include <memory>
class MyClass {
public:
    ~MyClass() { std::cout << "Destroying MyClass" << std::endl; }};
void shared_ptr_example() {
    // Creation method 1: direct initialization
    std::shared_ptr<MyClass> ptr1(new MyClass);
    // Creation method 2 (recommended): use make_shared (more efficient)
    auto ptr2 = std::make_shared<MyClass>();
    // Shared ownership
    std::shared_ptr<MyClass> ptr3 = ptr1;  // Reference count +1
    // Check reference count
    std::cout << "Use count: " << ptr1.use_count() << std::endl;  // Output: 2
    // Custom deleter (needs to use shared_ptr constructor)
    auto file_deleter = [](FILE* f) {
        if (f) fclose(f);
    };
    std::shared_ptr<FILE> file_ptr(fopen("test.txt", "r"), file_deleter);
}  // All shared_ptrs are destroyed when they go out of scope, reference count goes to 0

Circular Reference Problem

When two objects reference each other through shared_ptr, it leads to memory leaks:

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }};
class B {
public:
    std::shared_ptr<A> a_ptr;  // Should use std::weak_ptr to avoid circular reference
    ~B() { std::cout << "B destroyed" << std::endl; }};
void circular_reference() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;  // Circular reference: reference counts of a and b will never be 0
}  // Memory leak!

5. std::weak_ptr

Characteristics

  • Weak Reference: Does not control the object’s lifecycle, only observes the object managed by shared_ptr.

  • Solves Circular Reference: Breaks the circular dependency of shared_ptr.

  • Requires Conversion: Access to the object is obtained through lock() to get shared_ptr.

Example

class B;
class A {
public:
    std::weak_ptr<B> b_ptr;  // Use weak_ptr to avoid circular reference
    ~A() { std::cout << "A destroyed" << std::endl; }};
class B {
public:
    std::weak_ptr<A> a_ptr;  // Use weak_ptr to avoid circular reference
    ~B() { std::cout << "B destroyed" << std::endl; }};
void weak_ptr_example() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;  // No circular reference: reference counts of a and b are both 1
    // Use weak_ptr to access the object
    if (auto shared_b = a->b_ptr.lock()) {  // Check if the object exists
        std::cout << "b is still alive" << std::endl;
    }}  // a and b are destroyed normally

6. Common Methods of Smart Pointers

Method

Description

get()

Returns the raw pointer

reset()

Releases the currently managed object, can specify a new object

swap()

Swaps the contents of two smart pointers

use_count()

Returns the reference count (only for shared_ptr and weak_ptr)

expired()

Checks if the object has been destroyed (only for weak_ptr)

lock()

Gets shared_ptr from weak_ptr (only for weak_ptr)

7. Custom Deleters

Smart pointers allow specifying custom deleters for releasing non-memory resources:

// File handle management
auto file_deleter = [](FILE* f) {
    if (f) fclose(f);
};
std::unique_ptr<FILE, decltype(file_deleter)> file(fopen("test.txt", "r"), file_deleter);
// Array management
std::unique_ptr<int[]> array(new int[10]);  // Automatically uses delete[]
// Custom resource management
struct Resource {
    void acquire() { /* ... */ }
    void release() { /* ... */ }};
auto resource_deleter = [](Resource* r) {
    if (r) r->release();
};
std::unique_ptr<Resource, decltype(resource_deleter)> res(new Resource, resource_deleter);

8. Pitfalls and Precautions of Smart Pointers

1. Avoid mixing raw pointers and smart pointers:

   int* raw = new int;
   std::shared_ptr<int> ptr1(raw);
   std::shared_ptr<int> ptr2(raw);  // Error: double deletion!

2. Avoid creating shared_ptr from this:

   class Bad {
   public:
       std::shared_ptr<Bad> get_shared() {
           return std::shared_ptr<Bad>(this);  // Error: multiple independent shared_ptrs pointing to the same object
       }
   };  // Correct way: inherit from std::enable_shared_from_this

3. Prefer using make_shared and make_unique:

  • More efficient (single memory allocation).

  • Exception safe.

4. Understand special handling of arrays:

   // C++17 and later
   std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);  // Correct
   std::shared_ptr<int[]> arr2(new int[10], [](int* p) { delete[] p; });  // Needs custom deleter

9. Application Scenarios of Smart Pointers

  • Resource Management: File handles, network connections, locks, etc.

  • Container Elements: Storing polymorphic objects

    (e.g., std::vector<std::unique_ptr<Shape>>).

  • Avoiding Memory Leaks: In complex control flows or exception handling.

  • Cache Systems: Using weak_ptr to implement caches, avoiding impact on object lifecycle.

10. Improvements in C++20 and Later

std::make_shared_for_overwrite: Used for initializing objects that do not require default construction.

std::expected: Used in conjunction with smart pointers to handle error returns.

std::atomic<std::shared_ptr>: Atomic operation smart pointers for multi-threaded environments.

11. Conclusion

Smart pointers are core tools for managing dynamic memory in C++. Proper use of them can significantly enhance code safety and maintainability. It is recommended to follow these principles:

Prefer using std::unique_ptr: When object ownership is clear.

Use std::shared_ptr: When shared ownership is needed.

Use std::weak_ptr: When weak references or breaking circular references are needed.

Avoid using raw pointers: Unless necessary (e.g., for interface compatibility).

Leave a Comment