Unique Ownership and Custom Deleters (std::unique_ptr)
Smart Pointers and Memory Management

8.1 Unique Ownership and Custom Deleters (std::unique_ptr)

In C++, managing heap memory manually using raw pointers is a frequent source of bugs. Modern C++ solves this by introducing smart pointers. The simplest and most efficient smart pointer is std::unique_ptr (defined in the <memory> header), which implements the concept of exclusive ownership.

Let's look at unique pointers, copy constraints, and how to write custom deleters for non memory resources.

Exclusive Ownership: One Owner Only

A std::unique_ptr exclusively owns the resource it points to. Because ownership is unique, copying a unique pointer is explicitly blocked by the compiler (its copy constructor is deleted). If you were allowed to copy it, two unique pointers would point to the same memory, and both would try to delete it when exiting scope, causing a double free crash.

To transfer ownership of the resource to another unique pointer, you must use move semantics:

#include <iostream>
#include <memory>

struct Resource {
    Resource() { std::cout << "Acquired\n"; }
    ~Resource() { std::cout << "Destroyed\n"; }
};

int main() {
    // Create a unique pointer
    std::unique_ptr<Resource> ptr1{std::make_unique<Resource>()};

    // std::unique_ptr<Resource> ptr2{ptr1}; // COMPILER ERROR: Copying is blocked!

    // Transfer ownership using std::move
    std::unique_ptr<Resource> ptr2{std::move(ptr1)}; // ptr1 is now nullptr

    return 0;
} // Resource is automatically destroyed when ptr2 goes out of scope!
Spawning and moving unique pointers.

Zero Runtime Overhead

At the physical machine level, a std::unique_ptr is simply a wrapper around a raw pointer. It contains exactly one pointer member. This means the size of a std::unique_ptr in memory is exactly 8 bytes (on 64-bit systems), which is identical to a raw pointer! The compiler generates the delete call inline, yielding zero runtime memory or lookup overhead.

* The Deposit Box Metaphor: A unique pointer is like owning a single physical key to a deposit box. Only you hold it. You cannot duplicate the key (no copying). You can only hand the key over to someone else (moving). When you throw the key in the bin (the pointer goes out of scope), the deposit box is automatically locked and its contents are destroyed.

Custom Deleters: Managing Sockets and Files

By default, unique pointers release memory using the standard delete operator. However, you can pass a custom function or functor (called a custom deleter) to handle non memory resources, such as closing file handles, database connections, or network sockets when the pointer goes out of scope:

#include <iostream>
#include <memory>
#include <cstdio> // Required for FILE operations

// Custom deleter functor
struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp) {
            std::fclose(fp);
            std::cout << "File resource closed automatically via custom deleter!\n";
        }
    }
};

int main() {
    {
        // Open a file using standard C file operations
        // Stored inside std::unique_ptr with our FileCloser custom deleter!
        std::unique_ptr<FILE, FileCloser> filePtr{std::fopen("log.txt", "w"), FileCloser{}};

        if (filePtr) {
            std::fputs("Writing systems data...", filePtr.get());
        }
    } // filePtr exits scope here! FileCloser is called automatically, closing the file.
    return 0;
}
Using std::unique_ptr with custom file deleters.

The Custom Deleter Size Overhead Trap

While stateless custom deleters (like our FileCloser functor or a basic stateless lambda) add zero runtime size overhead to std::unique_ptr due to compiler Empty Base Optimization (EBO) keeping it at 8 bytes, passing a raw function pointer as a deleter changes the physical structure.

If you declare std::unique_ptr<FILE, void()(FILE)>, the stack footprint of the unique pointer doubles from 8 bytes to 16 bytes because it must physically store the function pointer address alongside the object address. Always prefer stateless functors or empty lambdas to maintain the zero cost abstraction layout.

Finished reading this lesson?