EduC++ Exclusive Ownership with std::unique_ptr

Exclusive Ownership with std::unique_ptr

Prerequisites: 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.

Helper class -- a resource that logs its lifetime

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

transfer ownership.  Attempting to copy is a compile error.

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

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

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

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.
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";
}

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;
}