SovranCode
Modern C++ Lesson 28
Course contents50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
Modern C++50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
PREVIOUS LESSONC++ Pointers and References: nullptr and Borrowing
NEXT LESSONC++ Move Semantics: std::move vs Copying
C++ foundations · Lesson 28 About 44 min with practice

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.

Before you begin

Understand pointers, nullptr, borrowing and RAII. Keep using ordinary values when they solve the problem; this lesson introduces dynamic ownership, not a requirement to allocate every object.

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.

After a completed unique_ptr move, an empty source tray and one destination tray show exactly one resource cube. The ownership-transfer arrow represents the move operation selected using std::move, not a second resource or a copy of the original object.
This empty-source guarantee belongs to a completed unique_ptr move. Evaluating std::move alone does not transfer or destroy the resource.

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.

C++ PRACTICE RUNNER

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.

#include <iostream>
#include <memory>
#include <utility>
int main() {
    auto source = std::make_unique<int>(7);
    auto destination = std::move(source);
    std::cout << "Source empty: " << (!source) << '\n';
    if (destination) std::cout << "Destination: " << *destination << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.
C++ PRACTICE RUNNER

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.

#include <memory>
int main() {
    auto first = std::make_unique<int>(7);
    auto second = first;
    return *second;
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

C++ PRACTICE RUNNER

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.

#include <iostream>
#include <memory>
int main() {
    auto owner = std::make_unique<int>(0);
    std::cout << "Has object: " << static_cast<bool>(owner) << '\n';
    owner.reset();
    std::cout << "Empty: " << (!owner) << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

LOCAL TERMINAL COMMANDS

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.exe
APPLY THE CONCEPT

Independent 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?
  1. Include memory and utility for the owning type and std::move.
  2. A completed unique_ptr move gives a known empty source, unlike some generic moved-from library objects.
  3. After reset, observe the owner rather than accessing an old borrowed pointer.
C++ PRACTICE RUNNER

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.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>

int main() {
    // Implement the acceptance checklist above.
    return 0;
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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
C++ PRACTICE RUNNER

Reference lab solution

Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.

#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
#include <memory>
#include <utility>
int main() {
    int value{};
    if (!readInteger(value) || value < -100 || value > 100) {
        std::cout << "Invalid input\n"; return 1;
    }
    auto source = std::make_unique<int>(value);
    auto destination = std::move(source);
    std::cout << "Source empty: " << (!source) << '\n';
    if (destination) std::cout << "Destination: " << *destination << '\n';
    destination.reset();
    std::cout << "Destination empty: " << (!destination) << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

  • C++ draft: unique ownership pointers
  • Microsoft: how to create and use unique_ptr
KNOWLEDGE CHECK

Check your understanding

Answer all five questions, then review the explanations. Answers are not saved or synced; completion is a separate self-review choice.

01What does unique_ptr primarily express?
02Why is ordinary unique_ptr copying disabled?
03Does get() give away ownership?
04How do reset() and release() differ?
05What is guaranteed after this completed unique_ptr move?
PREVIOUS LESSONC++ Pointers and References: nullptr and Borrowing
NEXT LESSONC++ Move Semantics: std::move vs Copying
ON THIS PAGELearning objectivesA raw address does not say who must clean upCreate the resource and check whether the owner contains anythingA move hands ownership to another ownerget borrows, reset cleans up, release gives up responsibilityTest the owner state as well as the valueIndependent labReviewKnowledge checkReferences
Course overview