Copying, Moving and Object Semantics
Learning Objectives
Distinguish copy construction from copy assignment, shallow from deep copy.
Write move operations and explain moved-from states; know that std::move is only a cast.
Apply the Rule of Three, Five and Zero.
Use the clone idiom for polymorphic copies and keep equality/hash contracts consistent.
Prerequisites: Modules 5, 8.
Intuition and Definition
C++ objects have value semantics by default: b = a copies. The class decides what "copy" means through its special member functions:
Copy constructor T(const T&) a new object is created from an existing one
Copy assignment T& operator=(const T&) an existing object is overwritten by another Move constructor T(T&&) a new object is created from an rvalue (temporary or std::moved) Move assignment T& operator=(T&&) an existing object takes over an rvalue's resources Destructor ~T() the object's lifetime ends
A shallow copy copies pointer values, so two objects point at one buffer (double delete!). A deep copy duplicates what the pointer owns. A move transfers ownership and leaves the source in a *valid but unspecified* state.
Rule of Three: if you write a destructor, copy constructor or copy assignment, you probably need all three.
Rule of Five: add the two move operations.
Rule of Zero: better still, own resources through members that already manage themselves (std::string, std::vector, std::unique_ptr) and write none of them.
Why It Matters
Getting copies wrong causes double frees, leaks and silent aliasing. Getting moves right turns expensive copies of large buffers into pointer swaps.
Syntax and Language Notes
Java, C#, Python and TypeScript have reference semantics for class objects: b = a copies the reference, not the object. Copies are explicit (clone(), copy constructors by convention, copy.deepcopy, structuredClone). C# structs and record with-expressions are the main value-semantics exceptions.
class Buffer {
public:
Buffer(const Buffer& other); // copy constructor
Buffer& operator=(const Buffer& other); // copy assignment
Buffer(Buffer&& other) noexcept; // move constructor
Buffer& operator=(Buffer&& other) noexcept; // move assignment
~Buffer();
};Complete Example
The Rule of Zero and the clone idiom for polymorphic copies:
#include <algorithm>
#include <cstring>
#include <iostream>
#include <utility>
class Buffer {
char* data_;
size_t size_;
public:
explicit Buffer(const char* s) : data_(new char[std::strlen(s) + 1]), size_(std::strlen(s)) {
std::memcpy(data_, s, size_ + 1);
std::cout << " construct \"" << data_ << "\"\n";
}
Buffer(const Buffer& o) : data_(new char[o.size_ + 1]), size_(o.size_) { // deep copy
std::memcpy(data_, o.data_, size_ + 1);
std::cout << " copy-construct \"" << data_ << "\"\n";
}
Buffer(Buffer&& o) noexcept : data_(std::exchange(o.data_, nullptr)), size_(std::exchange(o.size_, 0)) {
std::cout << " move-construct \"" << data_ << "\"\n";
}
Buffer& operator=(Buffer o) noexcept { // copy-and-swap: handles copy AND move assignment
std::swap(data_, o.data_);
std::swap(size_, o.size_);
std::cout << " assign -> \"" << data_ << "\"\n";
return *this;
}
~Buffer() { delete[] data_; }
const char* str() const { return data_ ? data_ : "(empty)"; }
};
Buffer makeGreeting() { return Buffer("hello"); } // guaranteed copy elision (C++17)
int main() {
s
…1. construct a
construct "alpha"
2. copy b from a
copy-construct "alpha"
3. move c from a
move-construct "alpha"
a is now (empty)
4. assign b = c (copy)
copy-construct "alpha"
assign -> "alpha"
5. return by value (elided, no copy or move)
construct "hello"
result: b=alpha c=alpha g=hello#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Shape {
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0; // polymorphic copy
virtual std::string name() const = 0;
};
struct Circle : Shape {
double r;
explicit Circle(double rr) : r(rr) {}
std::unique_ptr<Shape> clone() const override { return std::make_unique<Circle>(*this); }
std::string name() const override { return "circle r=" + std::to_string(r).substr(0, 3); }
};
// Rule of Zero: members manage themselves, so the compiler-generated copy/move are correct
class Drawing {
std::string title_;
std::vector<std::unique_ptr<Shape>> shapes_;
public:
explicit Drawing(std::string t) : title_(std::move(t)) {}
Drawing(const Drawing& o) : title_(o.title_) { // only needed because unique_ptr is move-only
for (const auto& s : o.shapes_) shapes_.push_back(s->clone());
}
Drawing(Drawing&&) noexcept = default;
void add(std::unique_ptr<Shape> s) { shapes_.push_back(std::move(s)); }
void retitle(std::string t) { title_ = std::move(t); }
void print() const { std::cout << title_ << ": "; for (const auto&
…original: circle r=1.5
copy: circle r=9.0Step-by-Step Walkthrough
Buffer b = a; calls the copy constructor: a new array is allocated (deep copy).
Buffer c = std::move(a); — std::move is just a cast to Buffer&&; the *move constructor* does the work, stealing a's pointer and leaving a empty but valid.
b = c; takes the parameter by value: c is copied into the parameter, then swapped into b, and the old contents die with the parameter. The same operator handles moves when given an rvalue (copy-and-swap idiom, strong exception guarantee).
makeGreeting() returns a prvalue; since C++17 the object is constructed directly in g — no copy, no move.
In m12_clone, copying Drawing calls clone() on each shape so the copy gets new Circle objects; changing the copy does not affect the original.
Common Mistakes
Shallow copy of an owning raw pointer (compiler-generated copy) → two objects delete the same memory.
Using an object after std::move as if it still had its value.
Forgetting noexcept on move operations — std::vector then copies instead of moving during reallocation, to keep its strong exception guarantee.
Self-assignment bugs in hand-written assignment (delete[] data_; copy from other.data_ when this == &other). Copy-and-swap avoids it.
Defining equality without a matching hash (Java equals without hashCode, C# Equals without GetHashCode): equal objects must have equal hashes or hash containers break.
Trade-offs and Alternatives
Prefer the Rule of Zero: let std::vector, std::string, std::unique_ptr manage resources.
Make polymorphic base classes non-copyable and provide clone() if copies are needed.
Equality contracts: equality must be reflexive, symmetric, transitive and consistent with hashing and ordering (a == b ⇒ hash(a) == hash(b); ordering consistent with equality if used in sorted containers).
Real-World Use
Image buffers, CAN frame logs and large matrices are moved, not copied, through processing pipelines. Game engines and embedded code often make objects that own hardware handles move-only (unique_ptr-like), so ownership is always clear.
Practice
Q1. (Easy) What does std::move(x) do by itself?
Answer: Nothing at run time — it casts x to an rvalue reference so that a move constructor/assignment can be selected.
Q2. (Medium) Why should move constructors be noexcept?
Answer: Containers like std::vector move elements during reallocation only if the move cannot throw (std::move_if_noexcept); otherwise they copy to preserve the strong exception guarantee.
Q3. (Advanced) Write a value class Version with ==, < and a hash that are mutually consistent, and use it in std::unordered_set and std::set.
#include <compare>
#include <functional>
#include <iostream>
#include <set>
#include <unordered_set>
struct Version {
int major, minor, patch;
auto operator<=>(const Version&) const = default; // generates == and ordering consistently
};
struct VersionHash {
size_t operator()(const Version& v) const {
return std::hash<int>{}(v.major) * 31 * 31 + std::hash<int>{}(v.minor) * 31 + std::hash<int>{}(v.patch);
}
};
int main() {
std::unordered_set<Version, VersionHash> seen{{1, 2, 0}, {1, 2, 0}, {2, 0, 0}};
std::set<Version> ordered{{2, 0, 0}, {1, 10, 0}, {1, 2, 5}};
std::cout << "unique: " << seen.size() << "\nordered:";
for (const auto& v : ordered) std::cout << " " << v.major << "." << v.minor << "." << v.patch;
std::cout << "\n";
}unique: 2
ordered: 1.2.5 1.10.0 2.0.0Interview Question
Q1. "What is the Rule of Zero, and when do you break it?"
Answer: Design classes so that they need no user-declared destructor, copy or move operations: resource ownership is delegated to members (RAII types) whose own special members are already correct, so the compiler-generated ones are too. You break it when the class itself is a resource manager (a custom handle wrapper), when you need different semantics (deep copy of a unique_ptr member via clone()), or when you want to forbid operations (= delete copy for a non-copyable polymorphic base). When you break it for one, think about all five (Rule of Five).
Quick Quiz
Q1. What state is a moved-from standard library object in?
Answer: Valid but unspecified — you may assign to it or destroy it, but don't rely on its value.
Q2. Which special member runs for Buffer b = a; — copy constructor or copy assignment?
Answer: Copy constructor (a new object is being created).
Q3. Does return Buffer("x"); copy or move in C++17?
Answer: Neither — guaranteed copy elision constructs the object directly in the destination.
Summary
Copy = duplicate, move = transfer, destructor = release; shallow copies of owning pointers are bugs.
Prefer the Rule of Zero; otherwise implement all five consistently, with noexcept moves.
Use clone() for polymorphic copies; keep equality, hashing and ordering consistent.
Related: Module 13 (smart pointers), Module 16 (operator overloading), Module 31 (Prototype).