C++ Module 12 – Move semantics and smart pointers
Expressing ownership in C++ function signatures
Choose parameter and return types that say who owns an object after a call: references lend, unique_ptr hands over, shared_ptr shares.
What you will learn
- Choose parameter types that state who owns what after a call
- Design factories that return ownership through std::unique_ptr
- Use C++26 std::indirect and std::polymorphic where the library provides them
Before you start
What you’ll learn · Before you start
What you will learn
- Choose parameter types that state who owns what after a call
- Design factories that return ownership through std::unique_ptr
- Use C++26 std::indirect and std::polymorphic where the library provides them
Before you start
On this page
void park(Vehicle* v) compiles whether the garage deletes the vehicle later or only looks at it, and in this
lesson’s first run it deleted a car that lived on the stack. A function’s signature can say who owns an object after
the call, and modern C++ uses it to say exactly that.
A reference or a raw pointer lends an object: the function uses it during the call and never deletes it. A
std::unique_ptr taken by value, a sink parameter: A parameter that takes over the object it is given, such as a std::unique_ptr taken by value: the caller writes std::move and is left empty, and the function keeps or destroys the object., hands the object over, and one returned from a
factory function: A function that creates and returns an object, often a static member that checks its arguments first and returns std::expected or std::optional, so that the class can keep its constructor private and every object valid. More in “Designing value types and strong types in C++” hands a new object to the caller. A std::shared_ptr taken by value means
the function keeps a share. With these types the compiler checks the rules that comments used to state, and a
reader can tell from a declaration alone whether a call transfers, shares or only borrows an object.
Each parameter type says who owns the object
Lend, hand over or share
Text description of the diagram
The diagram has three panels, one under the other. In each, a box for the caller has an arrow down to a function that receives the caller's Widget.
- Lend: the caller, still the owner, passes "a reference" to draw(const Widget&), which uses the widget during the call only.
- Hand over: the caller passes the widget with std::move(w) to park(std::unique_ptr<Widget>), which owns it from then on; the caller is left empty.
- Share: the caller passes "a copy" of its shared_ptr to keep(std::shared_ptr<Widget>), so the caller and the function both own the widget.
The diagram shows the three things a call can do with the caller’s widget. The C++ Core Guidelines give each signature
a rule; the table uses Widget for any class.
| Signature | What it tells the caller | Rule |
|---|---|---|
void f(const Widget&) |
Reads the widget during the call and keeps nothing | F.7 |
void f(Widget&) |
May change the widget, and keeps nothing | F.7 |
void f(const Widget*) |
Like the reference, but “no widget” (nullptr) is a valid argument |
F.60 |
void f(std::unique_ptr<Widget>) |
Takes ownership: the caller writes std::move(w) and is left empty |
R.32 |
void f(std::unique_ptr<Widget>&) |
May replace or reset the caller’s widget | R.33 |
void f(std::unique_ptr<Widget>&&) |
May take ownership, and the call alone does not show whether it did | F.18 |
void f(std::shared_ptr<Widget>) |
Keeps a share: it stores or passes on a copy | R.34 |
void f(std::shared_ptr<Widget>&) |
May point the caller’s shared_ptr at another widget | R.35 |
void f(const std::shared_ptr<Widget>&) |
Might keep a share; prefer const Widget& if it never does |
R.36 |
std::unique_ptr<Widget> make() |
The caller owns the new widget | F.26 |
const Widget* find() const |
Points into something another object owns; never delete it | F.42 |
Two rules cover most code. A function that only uses an object takes a reference, or a pointer when the object is optional; smart pointers appear in a signature only when the call changes who owns the object (F.7, R.30). And ownership never travels through a raw pointer or a reference (I.11), because nothing in such a type says that it does. Step through the questions that pick a type:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Only used. A function that reads the widget during the call and keeps nothing takes const Widget&, or Widget& if it changes it (F.7). Any caller can pass any widget, whatever owns it.
- Maybe no widget. When "no widget" is a valid argument, take const Widget* and handle nullptr (F.60). The pointer still owns nothing.
- Taken over. A function that keeps or destroys what it is given takes std::unique_ptr<Widget> by value (R.32). The caller writes std::move(w) and is left empty, so the hand-over shows at the call.
- Replacing the caller's. A function that may make the caller's pointer own another widget, or reset it, takes std::unique_ptr<Widget>& (R.33).
- Kept alongside. A function that stores a copy, or passes one on, while the caller goes on using the widget takes std::shared_ptr<Widget> by value (R.34).
- Handed out. A factory returns std::unique_ptr<Widget>: the caller owns the new widget (F.26) and may keep it unique or turn it into a shared_ptr.
Checkpoint
Check: What each parameter type says
Types turn an ownership crash into a compile error
A garage stores vehicles. In the first version, park(Vehicle*) looks like it borrows the vehicle, yet the
destructor deletes everything parked, so a caller who parks a vehicle that lives on the stack compiles cleanly and
crashes later. The second version says the same things in its types:
garage_unclear.cpp
// A garage whose signatures say nothing about ownership. Who deletes a parked vehicle, and may a caller delete what
// find() returns? Only the destructor, deep in the implementation, answers.
#include <print>
#include <string>
#include <utility>
#include <vector>
struct Vehicle {
std::string plate;
explicit Vehicle(std::string p) : plate(std::move(p)) {}
virtual ~Vehicle() = default;
};
class Garage {
public:
void park(Vehicle* v) { parked_.push_back(v); }
Vehicle* find(const std::string& plate) {
for (Vehicle* v : parked_)
if (v->plate == plate) return v;
return nullptr;
}
~Garage() {
for (Vehicle* v : parked_) delete v; // so park() took ownership after all
}
private:
std::vector<Vehicle*> parked_;
};
int main() {
Vehicle visitor("MH12CD5678"); // on the stack: main destroys it when it returns
Garage garage;
garage.park(new Vehicle("KA01AB1234")); // right: the garage deletes it
garage.park(&visitor); // compiles, and the garage will delete an object on the stack
std::println("found {}", garage.find("MH12CD5678")->plate);
} garage_clear.cpp
// The same garage with signatures that state ownership: park() takes it, find() lends a pointer, make_vehicle() hands
// a new vehicle to its caller.
#include <memory>
#include <print>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
struct Vehicle {
std::string plate;
explicit Vehicle(std::string p) : plate(std::move(p)) {}
virtual ~Vehicle() = default;
};
class Garage {
public:
void park(std::unique_ptr<Vehicle> v) { parked_.push_back(std::move(v)); } // takes ownership
const Vehicle* find(std::string_view plate) const { // lends: may be null, never delete it
for (const auto& v : parked_)
if (v->plate == plate) return v.get();
return nullptr;
}
private:
std::vector<std::unique_ptr<Vehicle>> parked_; // the vector deletes them: no destructor to write
};
std::unique_ptr<Vehicle> make_vehicle(std::string plate) { // a factory: the caller owns the result
return std::make_unique<Vehicle>(std::move(plate));
}
int main() {
Garage garage;
garage.park(make_vehicle("KA01AB1234"));
auto car = make_vehicle("TN09EF4321");
garage.park(std::move(car)); // the transfer is visible where it happens
std::println("car after park: {}", car ? "still owns a vehicle" : "empty");
if (const Vehicle* v = garage.find("TN09EF4321")) std::println("found {}", v->plate);
std::println("found MH12CD5678: {}", garage.find("MH12CD5678") != nullptr);
std::shared_ptr<Vehicle> pool_car = make_vehicle("DL03GH8765"); // a caller may share what a factory returns
std::println("pool car owners: {}", pool_car.use_count());
} garage_misuse.cpp
// The visitor mistake of garage_unclear.cpp, written against the clear signatures.
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Vehicle {
std::string plate;
explicit Vehicle(std::string p) : plate(std::move(p)) {}
virtual ~Vehicle() = default;
};
class Garage {
public:
void park(std::unique_ptr<Vehicle> v) { parked_.push_back(std::move(v)); }
private:
std::vector<std::unique_ptr<Vehicle>> parked_;
};
int main() {
Garage garage;
Vehicle visitor("MH12CD5678");
garage.park(&visitor);
} from session import run
# The unclear garage under AddressSanitizer, the clear one on both compilers, and the visitor mistake against the
# clear signatures (only the first error of each compiler is shown).
run("""
clang++ -std=c++23 -g -fsanitize=address garage_unclear.cpp -o garage_unclear && ./garage_unclear
clang++ -std=c++23 -Wall -Wextra garage_clear.cpp -o garage_clear && ./garage_clear
g++-16 -std=c++23 -Wall -Wextra garage_clear.cpp -o garage_clear-gcc && ./garage_clear-gcc
clang++ -std=c++23 garage_misuse.cpp 2>&1 | grep -m 1 'error:'
g++-16 -std=c++23 garage_misuse.cpp 2>&1 | grep -m 1 'error:'
""") Output
$ clang++ -std=c++23 -g -fsanitize=address garage_unclear.cpp -o garage_unclear && ./garage_unclear
found MH12CD5678
==…==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x… in thread T0
#1 in Vehicle::~Vehicle() garage_unclear.cpp:11
#2 in Garage::~Garage() garage_unclear.cpp:23
#3 in Garage::~Garage() garage_unclear.cpp:22
#4 in main garage_unclear.cpp:36
Address 0x… is located in stack of thread T0 at offset 32 in frame
#0 in main garage_unclear.cpp:30
SUMMARY: AddressSanitizer: bad-free
[12 more lines of the report left out]
==…==ABORTING
Abort trap: 6
[exit status 134]
$ clang++ -std=c++23 -Wall -Wextra garage_clear.cpp -o garage_clear && ./garage_clear
car after park: empty
found TN09EF4321
found MH12CD5678: false
pool car owners: 1
$ g++-16 -std=c++23 -Wall -Wextra garage_clear.cpp -o garage_clear-gcc && ./garage_clear-gcc
car after park: empty
found TN09EF4321
found MH12CD5678: false
pool car owners: 1
$ clang++ -std=c++23 garage_misuse.cpp 2>&1 | grep -m 1 'error:'
garage_misuse.cpp:24:17: error: no viable conversion from 'Vehicle *' to 'std::unique_ptr<Vehicle>'
$ g++-16 -std=c++23 garage_misuse.cpp 2>&1 | grep -m 1 'error:'
garage_misuse.cpp:24:17: error: cannot convert 'Vehicle*' to 'std::unique_ptr<Vehicle>'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 garage_session.py
Step through what happened:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A vehicle from new. park(new Vehicle("KA01AB1234")) is how the unclear garage expects to be used: the destructor will delete everything parked.
- The visitor parks too. park(&visitor) compiles just as well, because Vehicle* says nothing about who owns the vehicle. The visitor lives on main's stack.
- find() works. find("MH12CD5678") returns the visitor, and the run printed found MH12CD5678. Nothing is wrong yet.
- The destructor deletes it. When main returns, ~Garage deletes every pointer it holds, the visitor among them. AddressSanitizer stops the program: attempting free on an address in the stack of thread T0, a bad free.
- The clear signature. Against park(std::unique_ptr<Vehicle>), the same park(&visitor) does not compile, with either compiler: a raw pointer does not convert to a std::unique_ptr by itself.
- Clear calls, clear results. With the clear garage, park(std::move(car)) leaves car empty, find() lends a const Vehicle*, which is nullptr when no plate matches, and the vector of unique_ptr deletes the vehicles, so the garage needs no destructor.
AddressSanitizer stops the unclear garage in its destructor: it deleted the visitor’s vehicle, which was never
allocated with new, so the report is a bad free of an address in main‘s stack frame. The clear garage needs no
destructor at all, because its vector of std::unique_ptr deletes the vehicles. find() returns a pointer to a const
vehicle: a raw pointer owns nothing by the Core Guidelines’ convention (R.3), and const means that the caller cannot
change the vehicle through it. The same visitor mistake against the clear signatures is a compile error on both
compilers: a raw pointer does not convert to a std::unique_ptr by itself, so an ownership bug became a type error.
Factories return std::unique_ptr
make_vehicle() returns a std::unique_ptr, and that suits every caller. One can keep the vehicle unique or park it,
and another can turn it into a std::shared_ptr, as pool_car does in the run, starting with one owner. The reverse is
impossible: nothing turns a shared_ptr back into a unique_ptr. A factory returns std::shared_ptr only when it keeps a
share itself, for example a cache that tracks the objects it hands out.
Checkpoint
Check: Ownership in the types, and factories
C++26 adds owners that copy like values
Some classes need a heap-allocated part yet should copy like plain values. C++26 adds two owners for them. std::indirect holds one object of a known type and copies that object whenever it is copied. std::polymorphic holds an object of any class derived from a base, and a copy copies the derived object. GCC 16’s standard library ships both, while Apple clang 21’s libc++ does not have them yet. Step through a copy of each:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An indirect owner. std::indirect<Style> holds one Style on the heap, an object of a type known in advance.
- Copying it copies the Style. Copying p copies the Style it owns, and q owns that new Style: the two never share one.
- A polymorphic owner. std::polymorphic<Shape> holds an object of any class derived from Shape, here a Circle.
- The copy is a Circle too. Copying s copies the derived object, so t owns a Circle, not just the Shape part of one.
- Where they exist. Both types are C++26. GCC 16's standard library ships them, while Apple clang 21's libc++ does not have them yet.
Key idea
std::indirect and std::polymorphic make a deep copy: A copy that duplicates the resource itself, as a copy constructor does when it allocates a new array and copies the values into it, so the two objects own separate resources. More in “The rule of zero, three and five in C++” of the object they own whenever they are copied, so a class built from them copies like a value.
Checkpoint
Check: C++26 owners that copy like values
Practise
The weakest idea comes first.
Practice that adapts to you
Practise this lesson
14 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Where each signature goes wrong: three failures reproduced Pro: opens with a Pro, Premium or Ultimate pass
Each signature in the table fails in its own way when it is chosen wrongly or used carelessly. This part reproduces three of those failures with both compilers and ends with a refactoring exercise.
In this part:
- A reference whose owner the call resets
- By value or by &&: who owns a refused job
- What a shared_ptr parameter costs, counted
- Exercise: refactor a scheduler’s signatures
1 diagram · 8 runnable examples (C header, C++, Python, check.h, scheduler_test.cpp, starter/scheduler.h) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Owners that copy deeply: std::indirect and std::polymorphic on GCC 16 Pro: opens with a Pro, Premium or Ultimate pass
A class with a heap-allocated member must decide what copying it means. This part runs the usual choices side by side, then the two C++26 types that make a deep copy the default, with what to write where they are missing.
In this part:
- Copying a class with a smart-pointer member: shared state, or no copy at all
- std::indirect: one heap part that copies and stays const
- std::polymorphic: copying a class hierarchy without clone()
- Using them today, and what to write where the library lacks them
1 diagram · 2 runnable examples (Python, const_member.cpp, members.cpp, shapes.cpp) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
In modern C++, what does a raw pointer parameter such as
void draw(const Shape* s)tell the caller, and when would you take a reference instead?That
draw()uses the shape during the call and does not own it: it will not delete it and, unless its documentation says otherwise, will not keep the pointer after it returns. In code that follows the C++ Core Guidelines, raw pointers and references never own (R.3, R.4, I.11); ownership is written withstd::unique_ptrorstd::shared_ptr. A pointer says one thing a reference cannot: thatnullptr, "no shape", is a valid argument, which the function has to handle. When the argument is required, takeconst Shape&instead: it cannot be null, needs no check and accepts any shape, whatever owns it. A pointer is still the type for a non-owning member, because a reference member cannot be re-pointed. -
Mid to senior
A GUI framework calls on_click(const Screen& s) with *app.current, where current is a std::shared_ptr<Screen> member. Handlers may navigate, which assigns a new screen to current. Crashes are rare, and only some builds show them. Explain the bug, how to prove it, and where the fix belongs.
-
Fresher to mid
A library's factory returns std::shared_ptr<Parser>. One team wants to keep parsers in a std::vector<std::unique_ptr<Parser>> and cannot. What should the factory return, why does the change cost callers almost nothing, and when would returning shared_ptr be right?
Key takeaways
- References and raw pointers lend: the function uses the object during the call and never deletes it.
- Take std::unique_ptr by value to take ownership, and std::shared_ptr by value only to keep a share.
- Use smart pointers in a signature only when the call changes ownership; otherwise take
T&orT*. - Factories return std::unique_ptr, which callers can keep, move or turn into a shared_ptr.
- C++26’s std::indirect and std::polymorphic own a heap part and copy it like a value.
Terms in this lesson
- sink parameter
- A parameter that takes over the object it is given, such as a std::unique_ptr taken by value: the caller writes std::move and is left empty, and the function keeps or destroys the object.
References
- C++ Core Guidelines: F.7, for general use, take T* or T& arguments rather than smart pointers (Standard C++ Foundation)
- C++ Core Guidelines: I.11, never transfer ownership by a raw pointer (T*) or reference (T&) (Standard C++ Foundation)
- C++ Core Guidelines: R.30 to R.37, smart pointers as parameters (Standard C++ Foundation)
- C++ Core Guidelines: F.26, use a unique_ptr<T> to transfer ownership where a pointer is needed (Standard C++ Foundation)
- C++ Core Guidelines: F.18, for will-move-from parameters, pass by X&& and std::move the parameter (Standard C++ Foundation)
- C++ Core Guidelines: F.27, use a shared_ptr<T> to share ownership (Standard C++ Foundation)
- C++ Core Guidelines: F.60, prefer T* over T& when "no argument" is a valid option (Standard C++ Foundation)
- C++ Core Guidelines: F.42, return a T* to indicate a position (only) (Standard C++ Foundation)
- C++ Core Guidelines: R.3, a raw pointer (a T*) is non-owning (Standard C++ Foundation)
- P3019R14: indirect and polymorphic: Vocabulary Types for Composite Class Design (ISO/IEC JTC1/SC22/WG21)
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
- libc++ C++26 Status (LLVM Project)
- Working Draft, Programming Languages: C++ (N4950): [unique.ptr], [util.smartptr.shared] (ISO/IEC JTC1/SC22/WG21)
- AddressSanitizer (Clang documentation) (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress