Copy constructor, copy assignment, destructor, move semantics, and the Rule of Five. What happens when objects are born, cloned, transferred, and destroyed.
Consider a class that manages a dynamically allocated array. When you copy an object of that class, what happens to the array? Do you copy the pointer (both objects point to the same array)? Or do you copy the data (each object gets its own array)?
The answer depends on what you want. The compiler's default — copy every member — copies the pointer. Now two objects share the same array. Delete one, and the other has a dangling pointer. This is the shallow copy problem, and it's why copy control exists.
c++ class HasPtr { string *ps; int i; public: HasPtr(const string &s = string()) : ps(new string(s)), i(0) {} // Without copy control: compiler copies ps (pointer) // → two HasPtr objects share the same string! // → delete one → the other has a dangling pointer };
A copy constructor initializes a new object as a copy of an existing object. Its first parameter is a reference to const of the class type:
c++ class Foo { public: Foo(); // default constructor Foo(const Foo&); // copy constructor };
Copy initialization happens in more places than you might expect:
| Situation | Example |
|---|---|
Initialize with = | string s2 = s1; |
| Pass by value | void f(string s); |
| Return by value | string f() { return s; } |
| Brace-init a container | vector<string> v = {"a", "b"}; |
| Container insert/push | v.push_back(s); |
If you don't define one, the compiler generates a synthesized copy constructor that copies each non-static member using that member's own copy constructor (for class types) or direct copy (for built-in types). This is memberwise copy.
The copy-assignment operator is called when you assign one existing object to another:
c++ class HasPtr { public: HasPtr& operator=(const HasPtr &rhs) { auto newps = new string(*rhs.ps); // 1. copy the resource delete ps; // 2. free old resource ps = newps; // 3. point to new copy i = rhs.i; return *this; // 4. return *this } };
a = a), we'd destroy the data we're trying to copy. Always handle self-assignment correctly.An elegant pattern that handles self-assignment and provides strong exception safety:
c++ class HasPtr { friend void swap(HasPtr&, HasPtr&); public: // Note: parameter is by VALUE (makes a copy) HasPtr& operator=(HasPtr rhs) { // copy made here swap(*this, rhs); // swap our guts with the copy return *this; // rhs destroyed, frees old data } };
The destructor is called when an object is destroyed. It releases resources acquired during the object's lifetime:
c++ class HasPtr { public: ~HasPtr() { delete ps; } // free the dynamic string };
| Situation | What's destroyed |
|---|---|
| Variable goes out of scope | Local/automatic objects |
| Object is deleted | delete p; destroys *p |
| Container is destroyed | All elements |
| Temporary expression ends | Temporaries from expressions |
| shared_ptr ref count hits 0 | The managed object |
The destructor body runs first, then members are destroyed in reverse order of declaration. For class-type members, their own destructors are called. For built-in types (pointers, ints), nothing happens — which is why raw pointers to dynamic memory need explicit delete.
If a class needs a custom destructor, it almost certainly needs a custom copy constructor and copy-assignment operator too. This is the Rule of Three.
C++11 extended this to the Rule of Five: if you define any copy-control member, also define (or explicitly default/delete) the move constructor and move-assignment operator.
c++ class NoCopy { public: NoCopy() = default; // use synthesized default ctor NoCopy(const NoCopy&) = delete; // no copying allowed NoCopy& operator=(const NoCopy&) = delete; // no assignment ~NoCopy() = default; // use synthesized destructor };
| Syntax | Meaning |
|---|---|
= default | Use the compiler-generated version |
= delete | Prevent this operation entirely (compile error if called) |
iostream objects, for example, cannot be copied — what would it mean to copy cout? Use = delete to make this intent explicit and get a compile error instead of a runtime bug.When designing copy control, you have two fundamental choices: should copies of your class behave like values or like pointers?
| Value-like | Pointer-like | |
|---|---|---|
| Copy means | Deep copy (independent copies) | Shared state (reference counted) |
| Change one? | Other is unaffected | Both see the change |
| Examples | string, vector | shared_ptr, iostream |
| Resource mgmt | Each copy owns its own | Shared ownership with ref count |
Create an object, copy it, then modify the original. Watch how value-like copies are independent while pointer-like copies share state.
c++ // Deep copy: each HasPtr owns its own string HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i) {} HasPtr& operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; }
Copying is safe but expensive. When an object is about to be destroyed anyway (a temporary, or a variable being returned), why copy its resources? Just steal them. This is move semantics.
c++ class StrVec { public: // Move constructor: steal resources from rhs StrVec(StrVec &&rhs) noexcept : elements(rhs.elements), first_free(rhs.first_free), cap(rhs.cap) { // Leave rhs in a valid but unspecified state rhs.elements = rhs.first_free = rhs.cap = nullptr; } // Move assignment: free our resources, steal from rhs StrVec& operator=(StrVec &&rhs) noexcept { if (this != &rhs) { free(); // free our existing resources elements = rhs.elements; // steal rhs's resources first_free = rhs.first_free; cap = rhs.cap; rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; } };
Defining a custom swap for your class enables the copy-and-swap idiom and is used by standard algorithms. For classes that manage resources, swapping pointers is far cheaper than copying:
c++ class HasPtr { friend void swap(HasPtr&, HasPtr&); // ... }; inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // swap pointers, not string data swap(lhs.i, rhs.i); // swap ints }
using std::swap then an unqualified swap(a, b). This lets argument-dependent lookup find a type-specific swap if one exists, falling back to std::swap otherwise. Never write std::swap(a, b) directly — it bypasses type-specific optimizations.noexcept. If the move might throw, vector falls back to copying for safety. Always mark move operations noexcept if they don't throw.noexcept?Move semantics need a way to detect objects that are safe to move from. Enter rvalue references, declared with &&:
c++ int i = 42; int &r = i; // lvalue reference: binds to lvalue (named object) // int &&rr = i; // ERROR: can't bind rvalue ref to lvalue int &&rr = i * 42; // OK: bind rvalue ref to temporary (rvalue) int &&rr2 = std::move(i); // OK: move "casts" lvalue to rvalue
| Lvalue | Rvalue | |
|---|---|---|
| Identity | Has a name/address | Temporary, no persistent identity |
| Examples | x, *p, v[0] | 42, x + y, f() return |
| Can bind to | T& or const T& | T&& or const T& |
| Safe to move? | No (still in use) | Yes (about to die) |
c++ // Without move: expensive copy vector<string> v1 = getHugeVector(); // copies return value // With move: cheap pointer swap vector<string> v2 = std::move(v1); // steals v1's guts // v1 is now empty (valid but unspecified)
std::move actually do?Let's trace every copy-control operation as objects are created, copied, moved, assigned, and destroyed. This is how you debug copy-control issues in real code.
Perform operations and watch which special member functions are called. Each event appears in the log.
Copy control is the mechanism that makes C++ classes behave like built-in types. Get it right, and your classes compose seamlessly. Get it wrong, and you get crashes, leaks, and corruption.
| This Chapter | Builds Toward |
|---|---|
| Rule of Five | Designing any class that owns resources |
| Move semantics | Efficient return-by-value, container resizing |
| Rvalue references | Perfect forwarding in templates (Ch 16) |
| Value vs pointer classes | Designing class hierarchies (Ch 15) |
| Copy-and-swap | Exception-safe assignment everywhere |
Next up: Chapter 15: Object-Oriented Programming — inheritance, virtual functions, and dynamic binding.