C++ unique_ptr: Smart Pointers and Ownership
Learn C++ unique_ptr step by step: make_unique, exclusive ownership, checked access, moving ownership, get versus release, reset and automatic cleanup.
What you will learn
- Explain why a resource needs one clearly responsible owner.
- Create an owned object with make_unique rather than a raw new/delete pair.
- Transfer unique ownership without copying the resource.
- Distinguish borrowing with get from relinquishing ownership with release.
- Reset an owner without accessing an old borrow afterwards.
A raw address does not say who must clean up
The previous lookup borrowed an existing array element. Nothing needed a separate allocation, so a raw pointer was enough to describe temporary access. Sometimes an object needs a separately managed lifetime, or an interface needs to transfer responsibility for it. That introduces a different question: who will clean it up? std::unique_ptr answers that question with one owning object rather than a comment beside a raw address.
unique means there is only one owning unique_ptr for that resource at a time. Other code may still borrow access without owning it. This is not a claim that only one pointer value can exist anywhere. The distinction matters: a borrower must not delete the resource, while the owning smart pointer performs cleanup according to its deleter. In this lesson, the normal deleter destroys the owned object when ownership ends.
Do not allocate an integer dynamically just because you learned a new pointer type. An ordinary local int or an owned member is simpler when its normal lifetime already fits. We use make_unique<int> as a small teaching object because its value is easy to inspect. The pattern becomes useful for real ownership boundaries, not because dynamic allocation makes arithmetic better or because every class should contain a smart pointer.

Create the resource and check whether the owner contains anything
Include <memory> and write auto owner = std::make_unique<int>(7). The helper creates an integer initialized to seven and returns a unique_ptr that owns it. Prefer this clear construction to writing a raw new expression and then trying to attach an owner later. Allocation can still fail; smart ownership manages the lifetime of a successfully acquired resource rather than promising unlimited memory.
Use *owner to access the integer when ownership is present. For an owned class object, owner->member accesses a member of that object. These operations still need a target: an empty unique_ptr cannot be safely dereferenced. A bool test such as if (owner) checks whether the owner holds a pointer. It does not test whether an owned integer is nonzero. An owned zero is still an existing object.
When a unique_ptr leaves its normal scope, it disposes of what it owns. You do not write a matching delete in the caller. That would attempt to manage the same ownership twice and could cause invalid cleanup. The earlier RAII lesson explains the normal scope-exit paths and their limits. Smart ownership does not promise destructor execution after forced process termination, and it does not repair unrelated undefined behavior.
Move responsibility, then inspect the new owner
Read the output as a change in responsibility, not duplication of the integer. The moved-from unique_ptr is empty after this completed move construction.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Source empty: 1
Destination: 7
After editing inputs, calculate a new prediction; this reference describes only the starter.
A move hands ownership to another owner
An ordinary copy of unique_ptr is disabled. If two unrelated owners both believed they must delete the same integer, ordinary scope exit could try to clean it up twice. The intentional compile-error example rejects that design before execution. Repair it by deciding what the second variable needs: a non-owning borrow for temporary access, or an explicit ownership transfer with std::move. These are different requests, so do not pick one just to silence a diagnostic.
In the first example, destination is constructed from std::move(source). std::move marks the expression so a move operation can be selected; the move constructor performs the transfer. For unique_ptr, the source is empty afterwards and the destination owns the original object. The owned integer itself does not need to change address. A valid existing borrow may still identify that integer until the new owner destroys it.
A function taking unique_ptr<int> by value can express that it receives ownership. A function that only needs to read an integer usually should take const int& instead, not demand a smart pointer merely to inspect a value. A parameter type is part of the explanation of the interface. Ask whether the function observes, changes or takes responsibility for something, then select the smallest type that expresses that need.
- source initially owns the integer.
- The move constructor gives that same resource to destination.
- source becomes empty; it must not be dereferenced.
- destination later performs cleanup unless ownership moves again.
Repair an attempted copy of unique ownership
Intentional compile failure. Decide whether the second variable should borrow the integer or receive ownership; those repairs mean different things.
Predict the output, edit the source, then select Run code.
get borrows, reset cleans up, release gives up responsibility
get() returns the stored raw pointer without giving up ownership. Treat that result as a borrow. Do not build a second owning unique_ptr from it, and do not delete it yourself. Once the actual owner resets or destroys its resource, the old borrow stops being valid even if its raw pointer value is still non-null. Returning or storing a borrow therefore requires a clear lifetime agreement.
reset() without an argument disposes of the current resource and leaves the owner empty. Calling reset on an already empty owner is allowed. The second example uses reset and then checks the owner's empty state rather than following a stale address. If you replace an owned resource with another one, the old resource must also be disposed of; using an owning type keeps that responsibility explicit.
release() is different: it returns the pointer and stops owning it without deleting the object. Someone else must now take responsibility for that resource. Ignoring the returned pointer can leak it. This lesson does not need release at all, so do not use it as a longer spelling of reset. Reserve ownership handoff through raw pointers for interfaces that genuinely require it and document exactly who takes responsibility next.
End ownership without a manual delete
The owner is checked before access. After reset, the program observes only the owner's empty state, not the old integer.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Has object: 1
Empty: 1
After editing inputs, calculate a new prediction; this reference describes only the starter.
Test the owner state as well as the value
The lab reads one whole integer line and accepts values from minus one hundred through one hundred. Bad, missing or out-of-range input prints Invalid input and returns status one before an allocation is requested. For accepted input, create an owning integer, move ownership to a destination, print the source's empty state and the destination value, then reset the destination and print its empty state.
Use zero as one test, because it catches the mistake of treating the integer's value as proof of pointer absence. Also test both accepted endpoints, a numeric suffix, an overflowing integer token and a final valid line without a newline. Extra lines are outside this one-value demonstration and remain unread. We assume ordinary allocation succeeds; this small lab is not a complete memory-exhaustion recovery design.
A useful repair exercise starts from the rejected copy example. First explain how a raw borrow would differ from a transferred owner, then write each valid version as a separate complete program. Keep any borrow inside the final owner's valid lifetime. Never demonstrate the error by giving two smart pointers the same raw address and running them. The compiler's rejected copy and a safe corrected trace teach the ownership rule without executing double deletion.
Compile one program and compare its output
The lesson editors run complete programs only when the compiler sandbox is available. You can also save one example as main.cpp and use your installed compiler. Run only the repaired version of an intentional compile-error example.
macOS / Linux (choose your compiler):
clang++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
# Or: g++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
./lesson
Windows Developer Command Prompt:
cl /std:c++20 /EHsc /W4 main.cpp /Fe:lesson.exe
lesson.exeIndependent lab
Create one owned integer, transfer its ownership, inspect the valid destination, and end ownership with reset.
Acceptance checklist
- Read a complete integer in -100..100; otherwise print Invalid input and return 1 before allocation.
- Create the integer with make_unique.
- Move the owner to a destination instead of copying it.
- Print Source empty and Destination, checking the destination before dereference.
- Reset the destination and print Destination empty; use no raw new/delete or release.
Need a hint?
- Include memory and utility for the owning type and std::move.
- A completed unique_ptr move gives a known empty source, unlike some generic moved-from library objects.
- After reset, observe the owner rather than accessing an old borrowed pointer.
Write and run your lab
Build your own solution before revealing the reference. Use the input field for normal, boundary and invalid cases; this runner executes your program but does not grade it.
Predict the output, edit the source, then select Run code.
Your lab evidence and notes
Record predictions, actual output, commands and the cause of each repair. Code is plain text: this scratchpad does not execute or grade C++.
Loading local notes…
Review your acceptance checklist and quiz explanations before marking this lesson complete. This is self-review, not a grade or certification.
Reveal the explained lab solution
Reference lab solution
Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.
Predict the output, edit the source, then select Run code.
Expected output
Source empty: 1
Destination: 7
Destination empty: 1
Accepted input creates a real owned object. The source and destination are owner handles, not two allocated integers.
Move construction transfers responsibility and leaves the unique_ptr source empty; the known destination is checked before use.
Reset releases the owned resource and clears the destination. The example intentionally performs no access through a stale borrow afterwards.
Review before moving on
Explain the lesson's objectives in your own words, compare your lab evidence with the acceptance checklist, and keep the first failing case alongside your repair. Completing a page is not proof that every C++ feature or input is supported.
- Explain why a resource needs one clearly responsible owner.
- Create an owned object with make_unique rather than a raw new/delete pair.
- Transfer unique ownership without copying the resource.
- Distinguish borrowing with get from relinquishing ownership with release.
- Reset an owner without accessing an old borrow afterwards.
References and further reading
Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.