std::unique_ptr: Exclusive Ownership
Learn how std::unique_ptr provides zero-overhead exclusive ownership, how to use make_unique, custom deleters for C resources, and factory function patterns.
What Is unique_ptr?
std::unique_ptr is a smart pointer that owns an object exclusively — there is exactly one unique_ptr pointing to any given object. When the unique_ptr is destroyed (goes out of scope), it automatically deletes the owned object.
Key properties:
- Non-copyable — copying would create two owners, violating exclusive ownership.
- Moveable — ownership can be transferred via std::move.
- Zero overhead — sizeof(unique_ptr (with default deleter). The generated code is identical to raw pointer usage with manual delete.
- This is the default smart pointer. Reach for unique_ptr first; only use shared_ptr when you genuinely need shared ownership.
make_unique: The Only Correct Way
std::make_unique (C++14) is the only correct way to create a unique_ptr. It is exception-safe, concise, and avoids writing new.
#include <iostream>
#include <memory>
#include <string>
struct Connection {
std::string host;
int port;
Connection(std::string h, int p) : host(std::move(h)), port(p) {
std::cout << "Connected to " << host << ":" << port << '\n';
}
~Connection() {
std::cout << "Disconnected from " << host << '\n';
}
void query(const std::string& sql) {
std::cout << "Executing: " << sql << '\n';
}
};
int main() {
// CORRECT: use make_unique
auto conn = std::make_unique<Connection>("localhost", 5432);
conn->query("SELECT * FROM users");
// Access the raw pointer (non-owning)
Connection* raw = conn.get(); // does NOT transfer ownership
// Transfer ownership
auto conn2 = std::move(conn);
// conn is now nullptr
if (!conn) std::cout << "conn is null after move\n";
// conn2 destroyed here — destructor called automatically
} // Output: "Disconnected from localhost"Custom Deleters for C Resources
Many C libraries return raw pointers that must be freed with a specific function (not delete). unique_ptr supports custom deleters to handle these. The deleter becomes part of the type, so it has zero overhead when using a function pointer or stateless lambda.
#include <cstdio>
#include <memory>
#include <iostream>
// Custom deleter for FILE*
struct FileDeleter {
void operator()(FILE* f) const {
if (f) {
std::fclose(f);
std::cout << "File closed\n";
}
}
};
// Type alias for clarity
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;
// Factory function
UniqueFile open_file(const char* path, const char* mode) {
FILE* f = std::fopen(path, mode);
if (!f) throw std::runtime_error("Cannot open file");
return UniqueFile(f);
}
int main() {
try {
auto file = open_file("/tmp/test.txt", "w");
std::fputs("Hello, RAII!\n", file.get());
// file automatically closed when scope exits
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
// Lambda deleter (less common, but useful for one-offs)
auto deleter = [](int* p) { std::cout << "custom free\n"; delete p; };
std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
}unique_ptr with Arrays
std::unique_ptr manages a dynamically allocated array and calls delete[] automatically. It provides operator[] instead of operator* and operator->.
``cpp``
auto arr = std::make_unique
arr[0] = 42;
However, std::vector is almost always preferable — it knows its size, supports iteration, and is just as efficient. Use unique_ptr only when interfacing with C APIs or when you need a non-resizable, non-copyable buffer.
Factory Functions Returning unique_ptr
Returning unique_ptr from factory functions is the idiomatic C++ ownership pattern. The caller receives exclusive ownership. If the caller wants shared ownership, they can convert: std::shared_ptr.
#include <memory>
#include <iostream>
#include <string>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual std::string name() const = 0;
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
std::string name() const override { return "Rectangle"; }
};
// Factory — caller owns the result
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
if (type == "circle") return std::make_unique<Circle>(a);
if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
return nullptr; // unknown shape
}
int main() {
auto s = make_shape("circle", 5.0);
if (s) {
std::cout << s->name() << " area: " << s->area() << '\n';
}
// s automatically deleted here
}Never pass unique_ptr by const reference to a function that only observes the object — pass a raw pointer or reference instead. unique_ptr parameters signal ownership transfer.
- void use(Widget* w) — observes, no ownership
- void sink(std::unique_ptr — takes ownership (call with std::move)
- void reseat(std::unique_ptr — may replace the owned object
Never construct two unique_ptr from the same raw pointer — both will call delete, causing double-free. This is why make_unique is preferred: there is no raw pointer to accidentally reuse.
1. Default to unique_ptr — it should be your first choice for heap allocation.
2. Always use make_unique — never write unique_ptr.
3. Return unique_ptr from factories — it implicitly converts to shared_ptr if the caller needs it.
4. Pass by value to transfer ownership — void sink(unique_ptr. The caller must use std::move.
5. Pass raw pointer/reference to observe — void use(const T& obj) or void use(T* obj).
6. Mark move constructor/assignment noexcept — critical for types stored in containers.
unique_ptrprovides exclusive ownership with zero overhead vs raw pointers- Always create via
std::make_unique— never use raw(args...) new - Non-copyable but moveable — ownership transfers via
std::move - Custom deleters enable RAII for C resources (FILE*, sockets, etc.)
- Return
unique_ptrfrom factory functions — it converts toshared_ptrif needed - Pass raw pointer or reference when a function only observes, not owns
Quiz — Test Your Knowledge
(15 XP)1. What is the overhead of `std::unique_ptr<T>` compared to a raw `T*` (with default deleter)?
2. How should you pass a `unique_ptr` to a function that needs to take ownership?
3. Why is `std::make_unique` preferred over `unique_ptr<T>(new T(...))`?