C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
178: Implementing a Custom Smart Pointer from Scratch
When you first start writing your own resource managers in C++, it's easy to think that simply putting a delete call in the destructor is enough. I remember doing this early in my career, thinking I'd "solved" memory leaks, only to spend three hours debugging a segmentation fault that only happened when I passed my object into a function by value. Here is exactly what that looks like.
struct SimplePtr {
int* data;
SimplePtr(int val) : data(new int(val)) {}
~SimplePtr() { delete data; }
int& operator*() { return *data; }
};
void printValue(SimplePtr p) {
// p is passed by value, so a copy is made
std::cout << *p << std::endl;
} // p's destructor is called here!
int main() {
SimplePtr myPtr(42);
printValue(myPtr); // This looks fine...
std::cout << *myPtr << std::endl; // CRASH!
return 0;
}
The Double-Free Disaster
If you run the code above, it will likely crash. Why? Because we didn't define a copy constructor. C++ provides a default one that does a "shallow copy," meaning it just copies the memory address stored in data. When printValue finishes, the local copy p is destroyed, and it calls delete data. Now, the myPtr object in main is holding a pointer to memory that has already been freed. When main ends, it tries to delete that same memory again. That's a double-free, and it's a great way to get your program killed by the OS.
Enforcing Unique Ownership
If we want this to behave like a std::unique_ptr, we have to be explicit: this pointer cannot be copied. We don't want two objects thinking they "own" the same piece of memory. The fix is to use the = delete syntax on the copy constructor and copy assignment operator. This tells the compiler to throw an error if anyone tries to copy our pointer, catching the bug at compile-time instead of crashing at runtime.
struct UniquePtr {
int* data;
UniquePtr(int val) : data(new int(val)) {}
~UniquePtr() { delete data; }
// Stop the madness: No copying allowed
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
int& operator*() { return *data; }
int* operator->() { return data; }
};
Transferring Ownership via Move Semantics
Now we have a problem. We can't copy the pointer, but what if we actually want to move the resource from one object to another? This is where move semantics come in. We need a move constructor and a move assignment operator that "steal" the pointer from the source object and then set the source's pointer to nullptr. This ensures that only one object ever owns the memory, but we can still pass it around.
I personally find it helpful to think of the move constructor as a "hand-off." The old object lets go, and the new object takes over.
struct UniquePtr {
int* data;
UniquePtr(int val) : data(new int(val)) {}
~UniquePtr() { delete data; }
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
// Move constructor: Transfer ownership
UniquePtr(UniquePtr&& other) noexcept {
this->data = other.data;
other.data = nullptr; // The "hand-off"
}
// Move assignment operator
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete data; // Clean up existing resource
this->data = other.data;
other.data = nullptr;
}
return *this;
}
int& operator*() { return *data; }
int* operator->() { return data; }
};
By adding these move operations, we can now use std::move() to transfer our pointer into functions or containers without triggering a copy. We've successfully implemented the core logic of a smart pointer: RAII for automatic cleanup, deleted copies for safety, and move semantics for flexibility.
📋 Practical Task
Implementing a Reference-Counting SharedPtr
Now that you've mastered unique ownership, it's time to tackle shared ownership. Your task is to implement a simplified version of std::shared_ptr called SharedPtr.
Requirements:
- The
SharedPtrmust manage a dynamically allocated integer. - You must implement a separate
ref_count(a pointer to an integer on the heap) that is shared between all copies of theSharedPtr. - The copy constructor must increment the reference count.
- The destructor must decrement the reference count and only call
deleteon the managed data when the count reaches zero. - Overload
operator*andoperator->to allow access to the underlying data.
Testing your implementation:
Create three different SharedPtr objects that all point to the same integer (via copying). Print the value of the reference count at each step to ensure it increments and decrements correctly as the objects go out of scope.
There are no comments for now.