C++11 std::unique_ptr<T>: Exclusive ownership smart pointer
What is std::unique_ptr<T>?
C++11 std::unique_ptr<T>: Exclusive ownership smart pointer

What is std::unique_ptr<T>?
std::unique_ptr<T> is a smart pointer that manages a resource (which may be memory, a file handle, a socket, or a hardware mutex) through exclusive ownership. It acts as an RAII (Resource Acquisition Is Initialization) wrapper that guarantees the resource is released—via a deleter—exactly once: either when the unique_ptr<T> object goes out of scope or when it is reassigned.
Key characteristics:
- Exclusive Ownership: Only one
unique_ptrcan own a given resource at any time - Resource Management: Manages any resource, not just dynamically allocated memory (files, sockets, hardware resources, etc.)
- Guaranteed Cleanup: The resource is released exactly once through the deleter when the
unique_ptris destroyed or reassigned - Zero Overhead: No reference counting; essentially a wrapper around a raw pointer with minimal overhead
- Move-Only Semantics: Cannot be copied (to enforce exclusive ownership), but can be moved to transfer ownership
- RAII Principle: Follows the Resource Acquisition Is Initialization pattern, binding resource lifetime to object lifetime
Declaration in C++ Standard
According to the C++11 standard (and refined in later standards), std::unique_ptr is defined in the <memory> header:
#include <memory>
// Basic declaration
template<class T, class D = std::default_delete<T>> class unique_ptr;
// Partial specialization for array types
template<class T, class D> class unique_ptr<T[], D>;
The template has two parameters:
- T: The type of the object being managed
- D: The deleter (defaults to
std::default_delete<T>, which callsdeleteordelete[])
Ways to create and initialize std::unique_ptr<T>
- Using
new(C++11)
#include <memory>
#include <iostream>
class Dog {
public:
Dog(const std::string& name) : name_(name) {
std::cout << "Dog " << name_ << " created\n";
}
~Dog() {
std::cout << "Dog " << name_ << " destroyed\n";
}
private:
std::string name_;
};
int main() {
// Create a unique_ptr using new
std::unique_ptr<Dog> dog1(new Dog("Buddy"));
// Access the object
dog1->name();
// When dog1 goes out of scope, the Dog is automatically deleted
return 0;
}
- Using
std::make_unique<T>(C++14)
int main() {
// More safe and concise
auto dog2 = std::make_unique<Dog>("Max");
return 0;
}
Approach is recommended since its atomic and provide strong exception safety. The problems of naked new vs std::make_unique, i wil cover in depth in a separate post.
The most common way to access the managed object is through the dereference operators ( * or ->).
Non-Copyable Semantics
std::unique_ptr cannot be copied because it enforces exclusive ownership. Only one unique_ptr should manage a given resource.
When you try to copy a unique_ptr, the compiler will complain and wont allow to copy.
// Compilation ERROR!
std::unique_ptr<Dog> dog1 = std::make_unique<Dog>("Buddy");
std::unique_ptr<Dog> dog2 = dog1; // COMPILER ERROR: copy constructor deleted
std::unique_ptr<Dog> dog3(dog1); // COMPILER ERROR: copy constructor deleted
std::unique_ptr<Dog> dog4 = dog1; // COMPILER ERROR: copy constructor deleted
std::vector<std::unique_ptr<Dog>> dogs;
dogs.push_back(dog1); // COMPILER ERROR: cannot copy
The restriction exists for to provide safety and without this restriction below will be the issues:
// Without this restriction, this would be problematic:
std::unique_ptr<Dog> dog1 = std::make_unique<Dog>("Buddy");
std::unique_ptr<Dog> dog2 = dog1; // If copying were allowed...
// Now which one "owns" the Dog? Both?
// When dog1 goes out of scope, it deletes the Dog.
// When dog2 goes out of scope, it tries to delete the already-deleted Dog.
// Result: DOUBLE DELETE - memory corruption and crash!
This is achieved by deleting the copy constructor and copy assignment operator of std::unique_ptr class.
// Simplified view of unique_ptr definition:
template<class T>
class unique_ptr {
public:
// Copy operations are explicitly deleted
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// Move operations are available
unique_ptr(unique_ptr&&) noexcept;
unique_ptr& operator=(unique_ptr&&) noexcept;
// ... rest of implementation
};
Move Semantics
std::unique_ptr can be moved, which transfers ownership from one unique_ptr to another. When you move a std::unique_ptr object it invokes the move semantic special member functions.
Note:
std::move does not move anything it just converts the pased argument to a Rvalue reference causing invocation of move constructor or move assignment operator whichever fits the case.
Checkout this medium post for to understand move semantics and special member functions of move semantics: https://medium.com/@sagar.necindia/move-semantics-rvalues-and-move-constructors-3c5e88c87a21
int main() {
std::unique_ptr<Dog> dog1 = std::make_unique<Dog>("Buddy");
// Transfer ownership from dog1 to dog2
std::unique_ptr<Dog> dog2 = std::move(dog1);
// Now dog2 owns the Dog, dog1 is nullptr
if (dog1 == nullptr) {
std::cout << "dog1 is now null\n"; // This prints
}
// dog2 still owns the Dog
// When dog2 goes out of scope, the Dog is deleted
return 0;
}
Using std::move Explicitly:
void processDog(std::unique_ptr<Dog> dog) {
// Function takes ownership
std::cout << "Processing dog...\n";
// Dog is deleted when function returns
}
int main() {
std::unique_ptr<Dog> myDog = std::make_unique<Dog>("Max");
// Transfer ownership to the function
processDog(std::move(myDog));
// myDog is now nullptr
std::cout << "myDog after transfer: "
<< (myDog ? "valid" : "null") << "\n"; // Prints "null"
return 0;
}
Move in Return Values:
std::unique_ptr<Dog> createDog() {
auto dog = std::make_unique<Dog>("NewDog");
return dog; // Automatically moved (RVO or move semantics)
}
int main() {
std::unique_ptr<Dog> myDog = createDog();
// No copy, no extra allocations - just a move
return 0;
}
Move with Containers:
int main() {
std::vector<std::unique_ptr<Dog>> dogs;
dogs.push_back(std::make_unique<Dog>("Buddy")); // Moved into vector
auto dog = std::make_unique<Dog>("Max");
dogs.push_back(std::move(dog)); // Explicitly moved
// All dogs are automatically cleaned up when vector is destroyed
return 0;
}
Custom Deleters
By default, std::unique_ptr<T> uses std::default_delete<T>, which simply calls delete for pointers and delete[] for arrays. However, you can provide a custom deleter for specialized cleanup needs.
This is achieved by passing a functor or lambda function or simply a cleanup method as second argument to the unique_ptr<T> constructor.
Custom deleters are necessary when:
- Resource management differs from
delete: File handles, database connections, memory allocated withmalloc, etc. - Cleanup requires additional operations: Logging, reference counting, resource pool management
- Third-party library resources: APIs that require specific deallocation functions
Syntax for Custom Deleters:
// Template parameter specifies the deleter type
std::unique_ptr<T, DeleterType> ptr;
Here is an example of using custom deleter when we try to mange a file handler:
#include <cstdio>
#include <memory>
// Custom deleter for FILE*
struct FileDeleter {
void operator()(FILE* file) const {
if (file) {
std::cout << "Closing file...\n";
std::fclose(file);
}
}
};
int main() {
// FILE* requires fclose, not delete
std::unique_ptr<FILE, FileDeleter> file(
std::fopen("data.txt", "r")
);
if (file) {
// Use the file
char buffer[100];
std::fgets(buffer, sizeof(buffer), file.get());
}
// FileDeleter is called automatically, closing the file
return 0;
}
You can also pass a lambda as a deleter:
#include <memory>
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main() {
// Lambda as custom deleter
auto customDeleter = [](Resource* res) {
std::cout << "Custom cleanup before deletion\n";
delete res;
};
using ResourcePtr = std::unique_ptr<Resource, decltype(customDeleter)>;
ResourcePtr res(new Resource(), customDeleter);
// Output:
// Resource acquired
// Custom cleanup before deletion
// Resource destroyed
return 0;
}
Array Allocation and std::unique_ptr
std::unique_ptr has a partial specialization for arrays (unique_ptr<T[]>), which uses delete[] instead of delete:
#include <memory>
#include <iostream>
int main() {
// Single object
std::unique_ptr<int> single(new int(42));
// Array of objects - use T[]
std::unique_ptr<int[]> array(new int[100]);
// Access via operator[]
array[0] = 10;
array[99] = 20;
// Use make_unique for arrays (C++20)
auto modern_array = std::make_unique<double[]>(50);
modern_array[0] = 3.14;
// Automatic cleanup with delete[]
return 0;
}
Array with Custom Deleter:
struct ArrayDeleter {
void operator()(int* array) const {
std::cout << "Deleting array with custom deleter...\n";
delete[] array;
}
};
int main() {
std::unique_ptr<int[], ArrayDeleter> array(
new int[100],
ArrayDeleter{}
);
array[0] = 42;
return 0;
}
Reassigning std::unique_ptr with reset()
The reset() method allows you to reassign a unique_ptr to a new resource. When you reassign, the old resource is automatically deleted via the deleter, then the new resource is stored.
Here is a basic usage example of reset():
#include <memory>
#include <iostream>
class Animal {
public:
Animal(const std::string& name) : name_(name) {
std::cout << "Animal " << name_ << " created\n";
}
~Animal() {
std::cout << "Animal " << name_ << " destroyed\n";
}
private:
std::string name_;
};
int main() {
auto animal = std::make_unique<Animal>("Dog");
// Reset to a new resource
// First, the Dog is destroyed
// Then, the new Cat is stored
animal = std::make_unique<Animal>("Cat");
// Reset to nullptr (releases the resource without assigning new one)
animal.reset();
// Cat is destroyed
// animal is now nullptr
if (!animal) {
std::cout << "animal is now null\n";
}
return 0;
}
// Output:
// Animal Dog created
// Animal Dog destroyed
// Animal Cat created
// Animal Cat destroyed
// animal is now null
reset() with a Raw Pointer:
#include <memory>
#include <iostream>
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " acquired\n";
}
~Resource() {
std::cout << "Resource " << id_ << " released\n";
}
private:
int id_;
};
int main() {
std::unique_ptr<Resource> resource = std::make_unique<Resource>(1);
std::cout << "\nRessigning with reset()...\n";
// Reset with a new raw pointer
// Old resource (1) is destroyed first
resource.reset(new Resource(2));
std::cout << "\nCalling reset() with no arguments...\n";
// Reset with nullptr (default argument)
resource.reset();
std::cout << "\nEnd of main\n";
return 0;
}
// Output:
// Resource 1 acquired
//
// Reassigning with reset()...
// Resource 1 released
// Resource 2 acquired
//
// Calling reset() with no arguments...
// Resource 2 released
//
// End of main
reset() with Custom Deleter:
#include <memory>
#include <cstdio>
#include <iostream>
struct FileDeleter {
void operator()(FILE* file) const {
if (file) {
std::cout << "Closing file with custom deleter\n";
std::fclose(file);
}
}
};
int main() {
std::unique_ptr<FILE, FileDeleter> file(
std::fopen("data1.txt", "r")
);
if (file) {
std::cout << "Opened data1.txt\n";
}
// Reset to a different file
// data1.txt is closed with the custom deleter
// data2.txt is opened
file.reset(std::fopen("data2.txt", "r"));
if (file) {
std::cout << "Opened data2.txt\n";
}
// Close the file explicitly
file.reset();
return 0;
}
// Output:
// Opened data1.txt
// Closing file with custom deleter
// Opened data2.txt
// Closing file with custom deleter
Key Points About reset():
- Deletes old resource first: When you reassign, the old resource is deleted via the deleter before the new one is stored
- Safe with nullptr: Calling
reset()without arguments (orreset(nullptr)) safely releases the resource - Works with custom deleters: The deleter is applied when the old resource is destroyed
- Useful for resource replacement: Allows you to cleanly switch from one resource to another
- Enables cleanup without destruction: You can explicitly release a resource before the
unique_ptrgoes out of scope
Keep the below thing in mind when using std::unique_ptr:
- Use
std::make_uniqueby default when possible since its atomic. For More in-depth details ofnewvsstd::make_unique<T>you can check this medium post <> - Use
newwithstd::unique_ptrwhen:
- You need a custom deleter
- Wrapping a pre-existing pointer
- Working with C APIs
- Need to call private constructors (through friend mechanisms)
- Supporting C++14/C++17 with array types
3. Never mix approaches in the same codebase without clear reasoning
- Never try to get the raw pointer out of the std::unique_ptr using get() method if not really necessary(unavoidable) because get() just returns the raw pointer and does not transfer ownership so when the unique_ptr go out of scope it will release the holding resource pointer and can cause your program to crash if you are using the raw pointer beyond the lifetime of the actual std::unqiue_ptr.
Summary

std::unique_ptr is the best choice for exclusive ownership of dynamically allocated objects in modern C++.
For shared ownership smart pointer checkout this.
If you’re serious about passing technical interviews, try PracHub. It helped me structure my preparation with advanced mock sessions. [**Check it out here**] and start practicing. (Disclosure: This is an affiliate link).
Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.
메타데이터
- post_id
- 80d5a91bfc88
- slug
- c-11-exclusive-ownership-smart-pointer-std-unique-ptr-t-80d5a91bfc88
- url
- https://medium.com/@sagarmadala/c-11-exclusive-ownership-smart-pointer-std-unique-ptr-t-80d5a91bfc88
- canonical_url
- https://medium.com/@sagarmadala/c-11-exclusive-ownership-smart-pointer-std-unique-ptr-t-80d5a91bfc88
- author_url
- https://medium.com/@sagarmadala
- status
- ok
- fetched_at
- 2026-07-13 14:23:43