Rebuilding std::unique_ptr from Scratch — A Deep Dive into Ownership in C++
If you truly understand std::unique_ptr, you understand modern C++ memory management.
Rebuilding std::unique_ptr from Scratch — A Deep Dive into Ownership in C++

smart pointer in c++
If you truly understand
std::unique_ptr, you understand modern C++ memory management.
Most C++ developers use std::unique_ptr.
Only a few truly understand what guarantees it provides, why copy is deleted, why move exists, how deleters work, and what “exclusive ownership” actually means in production systems.
In this article, we won’t just talk about std::unique_ptr.
We will build our own UniquePtr from scratch, step by step — and along the way, explore:
- RAII and deterministic destruction
- Move semantics in practice
- Custom deleters
- Array specialization
- Real-world production use cases
- Common bugs and interview traps
- Threading implications
- Design trade-offs
By the end, you’ll not only understand std::unique_ptr — you’ll be able to implement it confidently.
1. The Real Problem: Manual Memory Management
Before C++11, this was common:
MyClass* ptr = new MyClass();
// ...
delete ptr;
And this was even more common:
MyClass* ptr = new MyClass();
if (something_failed()) {
return; // Memory leak since 'ptr' is not freed.
}
delete ptr;
Now imagine:
- Early returns
- Exceptions
- Multiple exit paths
- Complex ownership across modules
Memory leaks and double deletes were not edge cases — they were normal bugs.
2. RAII: The Philosophy Behind unique_ptr
RAII (Resource Acquisition Is Initialization) means:
The lifetime of a resource is tied to the lifetime of an object.
Instead of manually deleting memory, we let the destructor handle it automatically.
This is the philosophical foundation of std::unique_ptr.
3. Minimal Custom Implementation
Let’s start very simple.
template <typename T>
class UniquePtr {
private:
T* ptr;
public:
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
~UniquePtr() {
delete ptr;
}
T* get() const {
return ptr;
}
T& operator*() const {
return *ptr;
}
T* operator->() const {
return ptr;
}
};
This already gives us:
- Automatic destruction
- RAII behavior
- Pointer-like syntax
But we have a huge problem here…
4. The First Critical Rule: Delete Copy
If we allow copying:
UniquePtr<MyClass> p1(new MyClass);
UniquePtr<MyClass> p2 = p1; // Disaster
Now two objects (p1 & p2) think they own the same raw pointer.
Both destructors will call delete.
And booooom → Double free. Undefined behavior.
So we delete copy operations:
UniquePtr(const UniquePtr&) = delete; // Copy Constructor
UniquePtr& operator=(const UniquePtr&) = delete; // Copy Assignment
Now ownership is exclusive.
5. Move Semantics — The Heart of unique_ptr
unique_ptr is move-only. Ownership must be transferable.
UniquePtr(UniquePtr&& other) noexcept
: ptr(other.ptr) {
other.ptr = nullptr;
}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
Now this works:
UniquePtr<MyClass> p1(new MyClass);
UniquePtr<MyClass> p2 = std::move(p1);
After move:
p2owns the objectp1becomesnullptr
This is real ownership transfer.
6. Implementing reset(), release() , and swap()
- reset()
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
- release()
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
- swap()
void swap(UniquePtr& other) noexcept {
std::swap(ptr, other.ptr);
}
These are critical for resource lifecycle control.
7. Real-World Use Cases
Let’s move beyond theory.
7.1 Managing File Handles
UniquePtr<FILE> file(fopen("data.txt", "r"));
But wait — this will call delete, not fclose.
We need custom deleters.
8. Adding Custom Deleters
Real std::unique_ptr supports custom deleters.
Let’s extend our design.
template <typename T, typename Deleter = std::default_delete<T>>
class UniquePtr {
private:
T* ptr;
Deleter deleter;
public:
explicit UniquePtr(T* p = nullptr, Deleter d = Deleter())
: ptr(p), deleter(d) {}
~UniquePtr() {
if (ptr != nullptr) {
deleter(ptr);
}
}
// move constructor
UniquePtr(UniquePtr&& other) noexcept
: ptr(other.ptr), deleter(std::move(other.deleter)) {
other.ptr = nullptr;
}
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
};
Now:
auto fileDeleter = [](FILE* f) {
if (f != nullptr) {
fclose(f);
}
};
UniquePtr<FILE, decltype(fileDeleter)>
file(fopen("data.txt", "r"), fileDeleter);
This is production-grade behavior.
9. Array Specialization
delete vs delete[] matters.
Real std::unique_ptr has partial specialization:
template<typename T>
class UniquePtr<T[]> {
private:
T* ptr;
public:
~UniquePtr() {
delete[] ptr;
}
T& operator[](size_t i) {
return ptr[i];
}
};
Now:
UniquePtr<int[]> arr(new int[10]);
Without specialization, it would have caused undefined behavior.
10. Final Thoughts
Implementing UniquePtr teaches us:
- RAII deeply
- Move semantics practically
- Resource ownership design
- Why C++11 was revolutionary
The true power of std::unique_ptr isn’t just automatic delete.
It’s about making ownership a compile-time guarantee.
Once you internalize that, you stop thinking in terms of “when should I delete?” Instead, you start thinking in terms of:
Who owns this resource?
And that mindset shift is what separates modern C++ from legacy C++.
11. Closing Notes — Why This Matters to Me
I would like to end this with something personal.
For a long time in my career, I used std::unique_ptr the way most of us do.
Allocate. Move. Let it destruct. Done.
But the first time I tried implementing it myself, I realized something important:
unique_ptr is not just a smart pointer. It is a careful design philosophy.
When you work on some topics:
- High-performance backend systems (e.g. Blockchain)
- Embedded firmware
- Cryptographic modules
- Payment systems
You quickly learn that resource management is not optional — it is survival.
A leaked socket in production. A double-free in a crypto module. A forgotten buffer release in firmware.
These are not theoretical bugs.
Understanding std::unique_ptr deeply changed how I design APIs. Now when I see raw pointers in interfaces, I immediately ask myself:
Who owns this resource?
- Ownership clarity reduces bugs.
- Ownership clarity scales systems.
If this article helped you think differently about ownership in C++, feel free to share it — I enjoy discussing low-level systems design, memory models, and modern C++ abstractions.
Until next time — Happy Coding…
메타데이터
- post_id
- c41d34b7956f
- slug
- rebuilding-std-unique-ptr-from-scratch-a-deep-dive-into-ownership-in-c-c41d34b7956f
- url
- https://medium.com/@pkthapa/rebuilding-std-unique-ptr-from-scratch-a-deep-dive-into-ownership-in-c-c41d34b7956f
- canonical_url
- https://medium.com/@pkthapa/rebuilding-std-unique-ptr-from-scratch-a-deep-dive-into-ownership-in-c-c41d34b7956f
- author_url
- https://medium.com/@pkthapa
- status
- ok
- fetched_at
- 2026-07-13 06:23:13