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

7.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>
#include <utility>

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

int main() {
    std::unique_ptr<Resource> ptr1{std::make_unique<Resource>()};
    std::unique_ptr<Resource> ptr2{std::move(ptr1)};
    return 0;
}
Spawning and moving unique pointers.

Zero Runtime Overhead

A std::unique_ptr<T> with the default deleter is commonly the same size as a raw pointer, but the standard does not require a particular layout or byte size. A stateful deleter can increase its size. Its value is deterministic ownership and cleanup, not a promise about one machine representation.

  • 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.

Custom Deleter Size Overhead and Empty Base Optimization (EBO)

How does a stateless custom deleter (like our FileCloser functor) cost 0 bytes? Why doesn't the std::unique_ptr grow to 9 bytes (8 for the pointer + 1 for the empty functor class)?

The compiler applies Empty Base Optimization (EBO). It can compress empty class types down to 0 bytes when they are inherited or used in certain wrappers. With a stateless functor deleter, std::unique_ptr<T> is commonly the same size as a raw T* on mainstream implementations, but the standard does not require a particular size. A raw C function-pointer deleter typically adds a second pointer, often doubling the wrapper's size.

If you declare std::unique_ptr<FILE, void()(FILE)>, the wrapper often stores both the object pointer and the function pointer, commonly 16 bytes on 64-bit targets versus 8 for a default-deleter unique_ptr. Prefer stateless functors or empty lambdas when you need a custom deleter without extra storage.

Finished reading this lesson?