Move Semantics Summary
Review and test your understanding of all move semantics and resource management concepts covered in this chapter.
Ownership: the question this chapter kept asking
Every lesson in this chapter circles a single decision. When a resource travels from one part of your program to another, should the program duplicate it, hand it over, or merely look at it? Copy semantics duplicate. Move semantics hand over. Observers only look.
Instead of replaying the lessons one at a time, this recap is organised around that decision, because that is the shape the knowledge takes once you start writing real classes.
What each lesson contributed
| Lesson | The decision it covered | What to carry forward |
|---|---|---|
| Move Semantics Terminology | Vocabulary | Value categories, moved-from state, and the special member functions, in one lookup table |
| Automatic Memory Management | Who is responsible for delete |
A smart pointer is a composition class that owns a heap allocation and releases it from its destructor when the smart pointer goes out of scope |
| Understanding Rvalue References | How the compiler tells "duplicate" from "hand over" | T&& is a reference that binds only to r-values, which is the signal that makes move overloads selectable |
| Implementing Move Semantics | How to write the hand-over | A move constructor and move assignment operator steal the source's pointer and leave the source empty but valid |
| Casting to Rvalue References | Forcing a hand-over on a named object | std::move is a cast, not an operation |
| Exclusive Ownership Smart Pointers | Exactly one owner | std::unique_ptr deletes copying, enables moving, and is built with std::make_unique() |
| Shared Ownership Smart Pointers | Several owners at once | std::shared_ptr counts its owners and destroys the resource when the last one goes away |
| Solving Circular References with std::weak_ptr | Owners that trap each other | std::weak_ptr observes a shared resource without being counted as an owner |
L-values and r-values decide what is safe
The compiler does not guess whether stealing is acceptable. It reads the value category of the argument and applies a rule that is always safe.
| Argument | Examples | What the compiler is entitled to assume | Overload it selects |
|---|---|---|---|
| l-value | kick, array[2], *handle |
The object has a name, so later code may still read it | Copy |
| r-value | AudioClip{"tom", 120}, makeClip(), std::move(kick) |
The object is a temporary, or you have declared yourself finished with it | Move |
That is the whole justification for move semantics. A temporary is destroyed at the end of the full expression that created it, so reusing its buffer costs nothing and breaks nothing. A named object might be read on the next line, so the only safe default is to duplicate.
A variable of r-value reference type is itself an l-value. Given
AudioClip&& incoming, the parameter incoming has a name, so passing it along copies unless you write std::move(incoming).
The four hand-over functions in one program
This class implements all four so you can watch the compiler choose between them.
#include <iostream>
#include <string>
#include <string_view>
#include <utility>
class AudioClip
{
public:
AudioClip(std::string_view title, int frames)
: m_title{title}
, m_frames{frames}
, m_samples{new double[static_cast<std::size_t>(frames)]{}}
{
std::cout << "built " << m_title << '\n';
}
~AudioClip()
{
delete[] m_samples;
}
AudioClip(const AudioClip& source)
: m_title{source.m_title}
, m_frames{source.m_frames}
, m_samples{new double[static_cast<std::size_t>(source.m_frames)]{}}
{
std::cout << "copied " << m_title << '\n';
}
AudioClip& operator=(const AudioClip& source)
{
if (this == &source)
return *this;
delete[] m_samples;
m_title = source.m_title;
m_frames = source.m_frames;
m_samples = new double[static_cast<std::size_t>(source.m_frames)]{};
std::cout << "copy assigned " << m_title << '\n';
return *this;
}
AudioClip(AudioClip&& source) noexcept
: m_title{std::move(source.m_title)}
, m_frames{source.m_frames}
, m_samples{source.m_samples}
{
source.m_frames = 0;
source.m_samples = nullptr;
std::cout << "handed over " << m_title << '\n';
}
AudioClip& operator=(AudioClip&& source) noexcept
{
if (this == &source)
return *this;
delete[] m_samples;
m_title = std::move(source.m_title);
m_frames = source.m_frames;
m_samples = source.m_samples;
source.m_frames = 0;
source.m_samples = nullptr;
std::cout << "move assigned " << m_title << '\n';
return *this;
}
int frames() const { return m_frames; }
private:
std::string m_title{};
int m_frames{0};
double* m_samples{nullptr};
};
int main()
{
AudioClip kick{"kick", 240};
AudioClip snare{"snare", 480};
AudioClip backup{kick}; // argument is an l-value: copy constructor
AudioClip carried{std::move(snare)}; // argument is an r-value: move constructor
AudioClip slot{"silence", 8};
slot = backup; // argument is an l-value: copy assignment
slot = std::move(carried); // argument is an r-value: move assignment
std::cout << "slot now holds " << slot.frames() << " frames" << '\n';
return 0;
}
Output:
built kick
built snare
copied kick
handed over snare
built silence
copy assigned kick
move assigned snare
slot now holds 480 frames
The two copy paths call new a second time. The two move paths only reassign a pointer and blank the source, which is why moving is cheap no matter how large the buffer is.
| Function | Signature shape | Job |
|---|---|---|
| Copy constructor | AudioClip(const AudioClip&) |
Build a new object with its own allocation |
| Copy assignment | AudioClip& operator=(const AudioClip&) |
Release what we hold, then allocate and duplicate |
| Move constructor | AudioClip(AudioClip&&) noexcept |
Build a new object from the source's allocation, then empty the source |
| Move assignment | AudioClip& operator=(AudioClip&&) noexcept |
Release what we hold, adopt the source's allocation, then empty the source |
Mark move operations
noexcept, guard both assignment operators against self-assignment, and always leave the emptied source in a state that is safe to destroy or reassign.
The compiler will not write a correct move constructor for a class holding a raw owning pointer. An implicitly generated move simply copies the pointer, leaving two objects that both call
delete. Either write the move operations yourself or hold the resource in a member that already manages itself.
std::move changes the label, not the data
std::move performs no moving at all. It is a cast that relabels an l-value as an r-value so that overload resolution reaches the move overload. Nothing has been transferred until a move constructor or move assignment operator actually runs.
Apply
std::move only to a persistent object whose value you no longer need. After the move the object is in a valid but unspecified state: destroying it or assigning a fresh value to it is fine, reading its old value is not.
Picking an owner
| Situation | Reach for | Create it with | Copyable | Movable |
|---|---|---|---|---|
| One owner, no sharing | std::unique_ptr |
std::make_unique() |
No, copying is deleted | Yes |
| Several owners, resource lives until the last one goes | std::shared_ptr |
std::make_shared() |
Yes, and copying is how you share | Yes |
| Access to a shared resource without extending its life | std::weak_ptr |
Copy from a std::shared_ptr |
Yes | Yes |
| A function that only uses a resource it does not own | Plain reference or raw pointer | Nothing to create | n/a | n/a |
| Anything at all | Never std::auto_ptr |
It was deprecated and then removed in C++17 | n/a | n/a |
Moving a std::unique_ptr is the clearest demonstration of the whole chapter, because the class deletes its copy operations, so a transfer is the only thing that can happen.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
class MixTrack
{
public:
explicit MixTrack(std::string_view label)
: m_label{label}
{
std::cout << "track " << m_label << " opened" << '\n';
}
~MixTrack()
{
std::cout << "track " << m_label << " closed" << '\n';
}
const std::string& label() const { return m_label; }
private:
std::string m_label{};
};
int main()
{
std::cout << std::boolalpha;
std::unique_ptr<MixTrack> owner{std::make_unique<MixTrack>("vocals")};
std::unique_ptr<MixTrack> newOwner{std::move(owner)};
std::cout << "old handle empty: " << (owner == nullptr) << '\n';
std::cout << "new handle holds " << newOwner->label() << '\n';
std::shared_ptr<MixTrack> firstShare{std::make_shared<MixTrack>("drums")};
{
std::shared_ptr<MixTrack> secondShare{firstShare};
std::cout << "owners inside block: " << firstShare.use_count() << '\n';
}
std::cout << "owners after block: " << firstShare.use_count() << '\n';
return 0;
}
Output:
track vocals opened
old handle empty: true
new handle holds vocals
track drums opened
owners inside block: 2
owners after block: 1
track drums closed
track vocals closed
Two facts are worth pinning down from that run. Moving a std::unique_ptr leaves the original handle holding nullptr, so testing it is well defined and dereferencing it is not. And a std::shared_ptr copy raises the owner count while it exists and lowers it again on destruction, with the tracked object surviving until the count reaches zero.
Build shared ownership by copying an existing
std::shared_ptr, never by handing the same raw pointer to a second one. Two independently created owners each believe they are the only owner, and both will free the same memory.
When shared ownership turns into a leak
Reference counting has one failure mode: a group of std::shared_ptr members that reference each other keeps its own count above zero forever.
The program below is broken. It never destroys either object.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
class MixDesk
{
public:
explicit MixDesk(std::string_view name)
: m_name{name}
{
}
~MixDesk()
{
std::cout << m_name << " shut down" << '\n';
}
void pairWith(const std::shared_ptr<MixDesk>& partner)
{
m_partner = partner;
}
private:
std::string m_name{};
std::shared_ptr<MixDesk> m_partner{};
};
int main()
{
auto deck{std::make_shared<MixDesk>("deck")};
auto rack{std::make_shared<MixDesk>("rack")};
deck->pairWith(rack);
rack->pairWith(deck);
std::cout << "leaving main" << '\n';
return 0;
}
Output:
leaving main
Neither destructor message appears. When main ends, each object is still held by the other's m_partner, so neither count ever falls to zero and the memory is never released.
Switching the back reference to a std::weak_ptr fixes it. A weak pointer does not participate in the count, so it cannot keep anything alive. To use one you call lock(), which returns a std::shared_ptr that is empty if the resource is already gone.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
class MixDesk
{
public:
explicit MixDesk(std::string_view name)
: m_name{name}
{
}
~MixDesk()
{
std::cout << m_name << " shut down" << '\n';
}
void pairWith(const std::shared_ptr<MixDesk>& partner)
{
m_partner = partner;
}
void report() const
{
std::shared_ptr<MixDesk> partner{m_partner.lock()};
if (partner)
std::cout << m_name << " is paired with " << partner->m_name << '\n';
else
std::cout << m_name << " has no partner left" << '\n';
}
private:
std::string m_name{};
std::weak_ptr<MixDesk> m_partner{};
};
int main()
{
auto deck{std::make_shared<MixDesk>("deck")};
auto rack{std::make_shared<MixDesk>("rack")};
deck->pairWith(rack);
rack->pairWith(deck);
deck->report();
rack.reset();
deck->report();
std::cout << "leaving main" << '\n';
return 0;
}
Output:
deck is paired with rack
rack shut down
deck has no partner left
leaving main
deck shut down
Both objects are now released, and the surviving one notices that its partner has gone. Where you only need a yes-or-no answer, expired() reports whether the observed resource is still alive without producing a std::shared_ptr.
Pitfalls the chapter warned about
| Mistake | What goes wrong | Fix |
|---|---|---|
| Reading a moved-from object's value | The state is valid but unspecified, so the value is not yours to rely on | Assign a fresh value first, or do not move until you are finished |
| Relying on the implicit move for a raw owning pointer | The pointer is copied, and two objects free the same memory | Write the move operations, or store the resource in a self-managing member |
Building a second std::shared_ptr from a raw pointer |
Two separate counts, one allocation, one double free | Copy the existing std::shared_ptr |
Allocating with new and then wrapping it |
Loses the exception-safety guarantee that the factory functions give you | std::make_unique() and std::make_shared() |
Mutual std::shared_ptr members |
The cycle keeps its own count alive and the memory leaks | Make one direction a std::weak_ptr |
| Returning an r-value reference | Almost always leaves the caller with a reference to something already destroyed | Return by value and let move semantics handle the cost |
Summary
- A smart pointer is a composition class that owns heap memory and frees it from its destructor when it goes out of scope.
- Copy semantics duplicate a resource, move semantics transfer ownership of it, and the argument's value category decides which one the compiler picks.
- R-value references, written
T&&, bind only to r-values and are what make the move overloads reachable. std::moveis a cast that lets you invoke move semantics on an l-value you have finished with.- Deleting the copy constructor and copy assignment operator turns copying into a compile error, which is exactly how
std::unique_ptrenforces exclusive ownership. - Use
std::unique_ptrfor a single owner,std::shared_ptrfor shared ownership by reference count, andstd::weak_ptrto observe a shared resource without owning it. - Prefer
std::make_unique()andstd::make_shared()over wrapping the result ofnew.
Key Terminology
- Smart pointer: Composition class that owns a dynamic allocation and releases it in its destructor
- RAII: Tying a resource's lifetime to the lifetime of an object, so cleanup happens on scope exit
- Copy semantics: The rules that duplicate an object, implemented by the copy constructor and copy assignment operator
- Move semantics: The rules that transfer ownership, implemented by the move constructor and move assignment operator
- l-value: An expression naming an object that persists beyond the expression
- r-value: A temporary expression with no name of its own
- r-value reference: A reference declared with
&&that binds only to r-values - std::move: A cast that relabels an l-value as an r-value so move overloads become viable
- Move constructor: Builds a new object out of another object's resources
- Move assignment operator: Replaces an existing object's resources with another object's resources
- Moved-from state: The valid but unspecified condition of a source object after its resources are taken
- noexcept: A promise that a function will not throw, which the library relies on before choosing to move
- Deleted function: A member marked
= delete, used here to switch copying off entirely - std::auto_ptr: An early ownership type, deprecated in C++11 and removed in C++17
- std::unique_ptr: Exclusive-ownership smart pointer with copying deleted and moving enabled
- std::make_unique: The preferred factory for
std::unique_ptr - std::shared_ptr: Shared-ownership smart pointer that destroys the resource when its last owner goes away
- std::make_shared: The preferred factory for
std::shared_ptr - Reference counting: The tally of how many
std::shared_ptrcurrently own a resource - std::weak_ptr: Non-owning observer of a
std::shared_ptrresource, accessed throughlock() - Circular reference: A loop of owning pointers whose counts keep each other above zero
Looking Forward
You can now answer the ownership question for any resource your code touches: one owner, several owners, or a plain observer. That answer drives everything else, including which special member functions you write, which smart pointer you reach for, and where a leak could hide. Classes that get it right hand large buffers around at the cost of a pointer assignment and never need a manual delete.
The next chapter turns from a single object's resources to how objects relate to one another. Composition, aggregation, association, and dependency each imply a different ownership answer, and the tools from this chapter are how you express that answer in code.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Move Semantics Summary - Quiz
Test your understanding of the lesson.
Practice Exercises
Moveable Resource Class
Build a resource-managing class that implements move semantics for efficient transfers. Demonstrates move constructors, move assignment, std::move, and smart pointers.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!