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. Neverdeleteit, 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 wantedget().
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[]>callsdelete[], but preferstd::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.