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