← Back to list

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

What is std::unique_ptr<T>?

Sagar · 2026-01-08 05:24 · 1 claps · 7.8 min read
#cpp11 #smart-pointer #unique-ptr #cpp-programming #raii
Open on Medium ↗
Wiki topics: 💻 · Programming

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_ptr can 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_ptr is 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

Note: Manual memory management issues

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 calls delete or delete[])

Ways to create and initialize std::unique_ptr<T>

  1. 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;
}
  1. 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:

  1. Resource management differs from delete: File handles, database connections, memory allocated with malloc, etc.
  2. Cleanup requires additional operations: Logging, reference counting, resource pool management
  3. 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 (or reset(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_ptr goes out of scope

Keep the below thing in mind when using std::unique_ptr:

  1. Use std::make_unique by default when possible since its atomic. For More in-depth details of new vs std::make_unique<T> you can check this medium post <>
  2. Use new with std::unique_ptr when:
  • 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

  1. 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