Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 2 of 6

std::unique_ptr<T> owns a single object and destroys it when the pointer goes out of scope. There is exactly one owner, always.

#include <iostream>
#include <memory>

struct Resource {
    int id;

    Resource(int i) : id(i) { std::cout << "  acquire " << id << "\n"; }
    ~Resource() { std::cout << "  release " << id << "\n"; }
};

int main() {
    std::cout << "entering scope\n";
    {
        std::unique_ptr<Resource> p = std::make_unique<Resource>(1);
        std::cout << "  using " << p->id << "\n";
    }   // destroyed here, automatically
    std::cout << "left scope\n";
    return 0;
}
entering scope
  acquire 1
  using 1
  release 1
left scope

No delete. The destructor runs on every exit path — normal return, early return, or an exception (lesson 9.4). This is new/delete with the delete made unforgettable.

It cannot be copied

That is the whole design. Copying would create a second owner, which contradicts "unique":

#include <iostream>
#include <memory>

int main() {
    auto a = std::make_unique<int>(42);

    // auto b = a;              error: use of deleted function
    //                          'std::unique_ptr<...>::unique_ptr(const unique_ptr&)'

    auto b = std::move(a);      // transfer of ownership: explicit and visible

    std::cout << "b holds " << *b << "\n";
    std::cout << "a is now empty: " << (a == nullptr) << "\n";
    return 0;
}
b holds 42
a is now empty: 1

The 1 is true: after the move, a holds nullptr. Ownership moved to b, and a was left in the valid-but-empty state from lesson 9.9 — safe to destroy, safe to reassign, holding nothing.

The deleted copy constructor is the point. You cannot accidentally create a second owner, because the code that would do it does not compile. Compare that with a raw pointer, where int* b = a; compiles happily and gives you the double free from lesson 9.8.

Moving into and out of functions

#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Document {
    std::string title;
};

std::unique_ptr<Document> create(const std::string& title) {
    return std::make_unique<Document>(Document{title});    // moved out
}

void consume(std::unique_ptr<Document> doc) {
    std::cout << "consumed: " << doc->title << "\n";
}   // destroyed here

void inspect(const Document& doc) {
    std::cout << "inspected: " << doc.title << "\n";       // borrows only
}

int main() {
    auto doc = create("proposal");

    inspect(*doc);                  // still ours
    consume(std::move(doc));        // ownership handed over

    std::cout << "after consume, we hold nothing: " << (doc == nullptr) << "\n";
    return 0;
}
inspected: proposal
consumed: proposal
after consume, we hold nothing: 1

Returning a unique_ptr needs no std::move — returning a local moves automatically, and the compiler usually elides even that (lesson 9.10).

The API you will actually use

#include <iostream>
#include <memory>

int main() {
    auto p = std::make_unique<int>(7);

    std::cout << *p << "\n";              // dereference
    std::cout << (p ? "holds" : "empty") << "\n";   // contextual bool

    int* raw = p.get();                   // borrow the raw pointer, no transfer
    std::cout << *raw << "\n";

    p.reset(new int(9));                  // destroy the old, own the new
    std::cout << *p << "\n";

    p.reset();                            // destroy, become empty
    std::cout << (p ? "holds" : "empty") << "\n";

    auto q = std::make_unique<int>(3);
    int* released = q.release();          // give up ownership WITHOUT destroying
    std::cout << *released << "\n";
    delete released;                      // now it is your problem again

    return 0;
}
7
holds
7
9
empty
3

Two of these need care:

  • get() hands out a non-owning view. Never delete it, and never build another owner from it.
  • release() gives up ownership and returns the raw pointer without destroying. You now own it manually. It exists for handing objects to C APIs; if you find yourself calling it in ordinary code, you probably wanted get().

Arrays

std::unique_ptr<T[]> is a separate specialisation that calls delete[]:

#include <iostream>
#include <memory>

int main() {
    auto values = std::make_unique<int[]>(5);   // zero-initialised

    for (int i = 0; i < 5; ++i) {
        values[i] = i * i;                       // operator[] exists for arrays
    }

    for (int i = 0; i < 5; ++i) {
        std::cout << values[i] << " ";
    }
    std::cout << "\n";
    return 0;
}
0 1 4 9 16 

Correct, but you should use std::vector<int> instead almost every time: it knows its own size, can grow, and works with the algorithms in Module 13. unique_ptr<T[]> is for the rare case where you need a fixed-size buffer with no size tracking at all.

What it costs

Nothing. A unique_ptr holds one pointer, and its operations compile to the same instructions you would have written by hand:

#include <iostream>
#include <memory>

int main() {
    std::cout << "raw pointer:  " << sizeof(int*) << " bytes\n";
    std::cout << "unique_ptr:   " << sizeof(std::unique_ptr<int>) << " bytes\n";
    return 0;
}
raw pointer:  8 bytes
unique_ptr:   8 bytes

Same size, same speed, and the delete cannot be forgotten. There is no performance argument for raw owning pointers.

(That equality holds for the default deleter. A unique_ptr with a stateful custom deleter is larger, because it must store the deleter too.)

Custom deleters

For resources that are not freed with delete:

#include <iostream>
#include <memory>

struct Handle {
    int id;
};

Handle* open_handle(int id) {
    std::cout << "  opened " << id << "\n";
    return new Handle{id};
}

void close_handle(Handle* h) {
    if (h) {
        std::cout << "  closed " << h->id << "\n";
        delete h;
    }
}

int main() {
    {
        // The deleter's type is part of the unique_ptr's type.
        std::unique_ptr<Handle, decltype(&close_handle)> h(open_handle(7), &close_handle);
        std::cout << "  using " << h->id << "\n";
    }   // close_handle runs here

    std::cout << "done\n";
    return 0;
}
  opened 7
  using 7
  closed 7
done

This is how you wrap a C API — FILE*, a socket, a database connection — in RAII without writing the five functions from lesson 9.11.

Try it

Write a class Node for a singly linked list where each node owns the next via unique_ptr. Build a list of five, print it, and confirm the whole chain is destroyed. Then consider what happens with a list of a million nodes.

Checkpoint

  • std::unique_ptr<T> is a single owner that destroys its object on scope exit.
  • It cannot be copied, only moved — so transfers are explicit at the call site.
  • get() borrows without transferring; release() gives up ownership without destroying.
  • unique_ptr<T[]> calls delete[], but prefer std::vector.
  • It is the same size and speed as a raw pointer — there is no cost to using it.
  • A custom deleter wraps resources that are not freed with delete.

Questions about this lesson

Log in to ask a question.