EduC++ / Exclusive Ownership with std::unique_ptr

Exclusive Ownership with std::unique_ptr

Prereqs See 03_memory_management/raii/ first -- unique_ptr is an RAII wrapper for heap-allocated objects.
Standard C++11 (replaced the deprecated auto_ptr). C++14 added std::make_unique for safe construction.

Frequently Asked Questions

QWhat happens if I dereference a unique_ptr that has been moved from?
AAfter a move, the source unique_ptr holds nullptr. Dereferencing nullptr is undefined behaviour -- it will typically crash with a segfault, but the compiler is free to do anything. Always check for nullptr before dereferencing a unique_ptr that may have been moved from, or restructure your code so that moved-from pointers are never accessed (e.g., let them go out of scope immediately after the move).
QCan unique_ptr manage arrays?
AYes. Use std::unique_ptr<T[]> which calls delete[] instead of delete, and provides operator[] for indexed access. std::make_unique<T[]>(n) allocates an array of n default-initialised elements (C++14). However, in most cases std::vector<T> is a better choice because it provides bounds checking, iterators, and automatic resizing. Use unique_ptr<T[]> mainly when you need a fixed-size buffer with minimal overhead or when interfacing with C APIs that expect a raw array.
QWhat is the difference between release() and reset()?
Arelease() surrenders ownership and returns the raw pointer WITHOUT deleting the object. You are now responsible for deleting it yourself. If you discard the return value, the memory leaks. reset() destroys the currently managed object (calls delete) and optionally takes ownership of a new raw pointer. Use reset() when you want to destroy the current object or replace it; use release() only when you need to hand the raw pointer to a C API or another owner that will manage its lifetime.
QHow should I pass a unique_ptr to a function that does not take ownership?
ADo not pass the unique_ptr at all. Instead, pass a raw pointer (via .get()) or a reference (via *ptr) to the underlying object. This makes the function agnostic to ownership strategy and allows it to work with objects regardless of how they are managed. Pass unique_ptr by value only when you intend to transfer ownership into the function. Pass unique_ptr by reference only in rare cases where the function needs to reseat (reset or move) the caller's pointer.
C++
#include <iostream>
#include <memory>
#include <format>
#include <cstdio>

Helper class -- a resource that logs its lifetime

C++
class Resource {
    std::string name_;
public:
    explicit Resource(std::string name) : name_(std::move(name)) {
        std::cout << std::format("Resource '{}' acquired\n", name_);
    }
    ~Resource() {
        std::cout << std::format("Resource '{}' released\n", name_);
    }
    void use() const {
        std::cout << std::format("Using resource '{}'\n", name_);
    }
};
HOW UNIQUE_PTR WORKS INTERNALLY Deep Dive
 

Watch out: a stateful deleter (one that stores data) increases sizeof(unique_ptr) beyond sizeof(T*), because the deleter is stored inside the unique_ptr object itself (via empty-base optimisation for stateless deleters like function pointers). -----------------------------------------------

1 Transferring ownership to a function

What

Attempting to copy is a compile error.

When

Use this when it cleanly solves the problem in front of you.

Why

transfer ownership.

Use

Use it in code like: void transfer_ownership(std::unique_ptr<Resource> res) {.

C++ Version Not explicitly specified in this example.

transfer ownership. Attempting to copy is a compile error.

Watch out: unique_ptr cannot be copied -- use std::move() to

C++
void transfer_ownership(std::unique_ptr<Resource> res) {
    res->use();
    // res is automatically deleted when function exits
}

2 Returning unique_ptr from a factory function

What

-- it returns the raw pointer and relinquishes ownership, causing a leak if the return value is not captured.

When

-- it returns the raw pointer and relinquishes ownership, causing a leak if the return value is not captured.

Why

-- it returns the raw pointer and relinquishes ownership, causing a leak if the return value is not captured.

Use

Follow the code pattern in this section and keep usage explicit.

C++ Version Not explicitly specified in this example.

-- it returns the raw pointer and relinquishes ownership, causing a leak if the return value is not captured.

Watch out: never call .release() without assigning the result

HOW MAKE_UNIQUE IS EXCEPTION-SAFE Deep Dive
 
C++
std::unique_ptr<Resource> create_resource(const std::string& name) {
    // No std::move needed -- the compiler applies copy elision / implicit move
    return std::make_unique<Resource>(name);
}

3 Custom deleter example

What

Providing a custom deleter to unique_ptr so it can manage non-new resources like C-style FILE* handles.

When

When wrapping C library resources (FILE*, sqlite3*, HANDLE) that require a specific cleanup function instead of delete.

Why

It improves clarity and helps prevent common correctness mistakes.

Use

Follow the code pattern shown in this section and adapt it to your types.

C++ Version C++11+ (file discusses C++14, C++17)

unique_ptr can manage any resource -- not just heap objects -- by providing a custom deleter. Here we wrap a C-style FILE* so that fclose is called automatically when the unique_ptr goes out of scope.

C++
void demonstrate_custom_deleter() {
    std::cout << "\n--- Custom deleter (FILE*) ---\n";

    // Lambda deleter: closes the file and logs
    auto file_deleter = [](FILE* fp) {
        if (fp) {
            std::cout << std::format("Custom deleter: closing FILE*\n");
            std::fclose(fp);
        }
    };

    // unique_ptr<FILE, decltype(lambda)> -- the deleter type is part of
    // the unique_ptr type.  Because the lambda is stateless (captures
    // nothing), the empty-base optimisation keeps sizeof == sizeof(FILE*).
    {
        auto file = std::unique_ptr<FILE, decltype(file_deleter)>(
            std::fopen("unique_ptr_demo.txt", "w"), file_deleter);

        if (file) {
            std::fputs("Hello from unique_ptr with custom deleter!\n", file.get());
            std::cout << std::format("Wrote to file via unique_ptr<FILE*>\n");
        }
        // file goes out of scope here -> file_deleter is called -> fclose
    }
    std::cout << "File automatically closed by custom deleter\n";
}

C++
int main() {
    // Create with make_unique (preferred)
    auto res1 = std::make_unique<Resource>("Database");
    res1->use();

    // Transfer ownership (move, not copy)
    auto res2 = std::move(res1);
    // res1 is now nullptr!

    if (!res1) {
        std::cout << "res1 is now empty after move\n";
    }

    // Pass ownership to function
    transfer_ownership(std::move(res2));

    // Factory function returning unique_ptr
    auto res3 = create_resource("Network");
    res3->use();
    // res3 cleaned up automatically at end of scope

    // Custom deleter demonstration
    demonstrate_custom_deleter();

    std::cout << "Back in main - resources auto-cleaned!\n";
    return 0;
}