Skip to content
VLSI Mentor

C++ Part I · Module 13 · Copy and Move Semantics

The Rule of Three

Four chapters have now produced a type that is correct for some operations and broken for others, which is a worse situation than the one they started from. Chapter 13.1 measured a class with a destructor and generated copies producing one buffer with two owners and a process killed by a signal. Chapter 13.2 repaired construction, and Chapter 13.4 showed the repaired picture — two buffers, one arrow each, a clean run. But the same class still has a generated copy assignment, and Chapter 13.3 measured what that does: it reproduces the sharing and leaks the destination's original buffer as well, which a live-buffer counter showed as one allocation still outstanding after both objects had been destroyed. So the type now fails on assignment rather than on construction, which is harder to notice and no better. The pattern is the chapter: these three functions are not three independent choices. They are three answers to a single question about ownership, and answering it in one place while leaving the compiler to answer it in the others produces a type whose correctness depends on which operations a caller happens to use. The rule that follows is one sentence, and the compiler turns out to hold the same opinion — in a diagnostic that states it almost in words.

Intermediate17 min readC++Part IFundamentalsCopy Semantics

Part I · Module 13 · Chapter 13.5 · The Rule of Three

1. The Rule

If you had to write any one of the destructor, the copy constructor, or the copy assignment operator, you almost certainly need all three.

That is the Rule of Three. It is a heuristic rather than a language rule — nothing refuses to compile — and the rest of this chapter is why it holds, because the reasoning is more useful than the statement.

2. The Three Functions and the One Question

Each of the three is an answer to the same question, asked at a different moment:

FunctionThe momentThe question
destructorthis object endswhat do I release?
copy constructora new object is made from mewhat does a duplicate of me own?
copy assignmentan existing object becomes a copy of mesame, plus: what does the destination release first?

All three are answers to: what does this object own?

And the compiler generates answers to all three whether you supply them or not — Chapter 12.4 §1 measured all five special members present on a type with no user-declared functions. So the three answers always exist. The only question is whether they agree.

3. Why One Implies the Others

Follow the reasoning in the direction it actually arises.

You wrote a destructor. Why? Chapter 12.7's callout gave the test: because the object owns a resource that must be released. If it owned nothing, you would not have written one.

So the object owns something. Chapter 13.1 §7's question is answered: yes, through a member that does not manage itself — otherwise the member's own destructor would have sufficed.

So the generated copy constructor is wrong. Chapter 13.1 §2's rule is memberwise, so it duplicates the handle and not the resource, producing Chapter 13.4's picture: two owners, one resource.

And the generated copy assignment is wrong twice over. It produces the same sharing and it drops whatever the destination was holding, because a memberwise assignment overwrites the handle without releasing what it pointed at.

Each step follows from the one before, which is why the rule is stated as "almost certainly" rather than "possibly".

4. The Three Incomplete Combinations, Measured

What you wroteWhat happensMeasured
destructor onlyshared resource, double freeexit 133; ASan double-free … in ~Buffer(); 0 compiler diagnostics
destructor + copy constructorassignment still shares and leaks1 buffer still live after both objects destroyed (3 live where 2 expected)
all threecorrectexit 0, ASan clean, self-assignment safe

The middle row is the one worth dwelling on, because it is the state a partially-repaired class is in and it is the least obvious.

Chapter 13.3 §8's counter measured it: two objects, three live buffers, and after both destructors had run one buffer was still outstanding. The class copies correctly and assigns incorrectly, so whether a program using it is correct depends on whether anyone writes b = a rather than Buffer b = a. Chapter 13.2 §7 noted those two lines differ by where the = sits.

And Chapter 13.3 §8 measured a second failure in the same row: with the release-before-allocate ordering and a throwing allocation, the live counter went to −1 — more releases than acquisitions, which is the double free expressed as arithmetic.

5. The Fourth Option Is Part of the Rule

The rule says you need all three, and it does not say you must write all three. Chapter 12.4 §7 measured the alternative:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer(const Buffer&)            = delete;
    Buffer& operator=(const Buffer&) = delete;

That is a complete and honest answer to the question: a duplicate of me is not a thing that can exist. The three functions now agree — the destructor releases, and copying is refused — and the type is correct, just restricted.

Chapter 12.9 §9 drew the line on when this is right: permanently correct when the resource represents a unique claim (a lock, a file handle, a scoped registration), and a placeholder when the resource is merely data that happens to be dynamically allocated. The deleted version of Chapter 10.8's Buffer could not be returned from a factory or stored in a container, which Chapter 12.9 called honest and restricted.

So the rule in practice is: all three must have consistent answers, and = delete is a valid answer. What is never valid is three answers written by two different authors — you and the compiler.

6. The Compiler Agrees

This is the chapter's most interesting measurement, because the compiler states the rule itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct D { int x; ~D() {} };
    D a{1};
    D b = a;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: definition of implicit copy constructor for 'D' is deprecated
         because it has a user-provided destructor
         [-Wdeprecated-copy-with-user-provided-dtor]

"Deprecated because it has a user-provided destructor." That is the Rule of Three, in a diagnostic. The standard itself deprecates the implicit copy operations for a type with a user-declared destructor, on precisely the reasoning in §3 — if you wrote a destructor, the generated copy probably means the wrong thing.

Two honest qualifications:

It is not in the standing flag set. The warning required -Wdeprecated-copy-dtor explicitly; at -Wall -Wextra -Wpedantic the class in Chapter 13.1 §4 compiled with 0 diagnostics. So the compiler holds the opinion and does not volunteer it.

And "deprecated" means still generated. The copy constructor exists and works — shallowly — which is why Chapter 13.1 §4's program compiled and ran to its double free. Deprecation is a statement about intent, not a removal.

The practical consequence: add -Wdeprecated-copy-dtor to your build. It is the one automated check that catches a Rule of Three violation, and this module has otherwise measured zero diagnostics for every failure in it.

7. Applying It

The rule is a checklist with one question at the top.

Does this class own a resource through a member that does not manage itself?

If no — write none of the three. The generated ones are correct, and Chapter 13.1 §3 measured that working.

If yes — then all three, consistently, choosing one of two shapes:

Duplicate. A deep copy constructor (Chapter 13.2 §2) and a copy assignment with the guard-allocate-release-commit order (Chapter 13.3 §7). The type behaves like a value.

Or refuse. = delete on both copy operations. The type behaves like a unique claim.

And check the three agree by asking the destructor's question of each: the destructor releases X, so a copy must have its own X, and an assignment must release its old X before taking on a new one.

8. One Omission, Deliberate

Everything above concerns three functions, and Chapter 12.4 §1 measured five special members. The two not mentioned are the move operations, and there is a reason the rule has a name that counts to three: it predates them. Before C++11 there were three, and the rule was complete.

With moves in the language the picture changes in two ways, and both belong to Chapter 13.10:

There is more to write — five functions rather than three, which is why that chapter is called the Rule of Five.

And declaring some of them silently removes others. Chapter 13.10 measures this, and the result is sharper than the Rule of Three: writing a destructor does not merely deprecate the copy operations, it suppresses the generated move operations entirely, so a class that follows the Rule of Three and stops there quietly loses the ability to move and falls back to copying everywhere — which Chapter 13.2 §6 measured as 2n − 1 copies for n container insertions rather than n.

So the Rule of Three is correct and incomplete, and it is worth learning in this form first because the reasoning in §3 is the part that generalises.

9. The Better Answer

Stated now because the rule invites the wrong conclusion, which is that resource-owning classes are normal and you should get good at writing five functions.

Every measurement in this module has been on a class holding a raw int*. Replace that member with a std::vector<int> and all of it goes away: the generated destructor releases the vector, the generated copy constructor copies it — Chapter 13.1 §3 measured that delegation working for std::string — and the generated assignment does Chapter 13.3 §7's correct sequence, because somebody implemented std::vector::operator= carefully once.

The number of these functions you should write is usually zero, and Chapter 13.11 is that argument in full. The Rule of Three tells you what to do when you cannot avoid owning a raw resource. Chapter 13.11 tells you how rarely that is.

10. What to Actually Do

Ask whether the class owns a resource through a member that does not manage itself. That decides everything.

If it does not, write none of the three.

If it does, write or delete all three together — as one edit, never one at a time.

Choose duplicate or refuse, and make all three agree with that choice.

Use = delete when the resource is a unique claim — it is a complete answer.

Add -Wdeprecated-copy-dtor to your build; it is the only automated check here.

Do not trust the standing flags — measured 0 diagnostics for every failure in this module.

Review a diff that adds one of the three as incomplete.

And prefer self-managing members so that the count is zero (Chapter 13.11).

11. Mistakes Worth Making Once

Writing a destructor and stopping. Measured: one buffer, two owners, exit 133.

Writing a destructor and a copy constructor and stopping. Measured: 1 buffer still live after both objects were destroyed, plus the sharing returns on assignment.

Believing the half-repaired state is safer. It is harder to find, and the correct-looking copy constructor discourages the next reader from checking.

Assuming the compiler will tell you. It holds the opinion and does not volunteer it without -Wdeprecated-copy-dtor.

Reading "deprecated" as "removed". It is still generated and still shallow.

Forgetting = delete counts as an answer. It is a complete and often correct one.

Adding one of the three in a diff and treating the change as finished.

Learning the rule and stopping at three. Moves suppress and are suppressed — Chapter 13.10.

Getting good at writing these instead of avoiding them. Chapter 13.11.

12. Interview Relevance

"What is the Rule of Three?" If you write any of destructor, copy constructor or copy assignment, you need all three. The answer that distinguishes gives the reasoning — all three answer "what does this object own?", so answering one implies the others are wrong by default.

"Why?" Because you only write a destructor if the object owns something, and if it owns something the memberwise copy shares it and the memberwise assignment both shares and leaks.

"Does the compiler help?" Yes, with -Wdeprecated-copy-dtor: definition of implicit copy constructor is deprecated because it has a user-provided destructor. Knowing this exists, and that it is not in -Wall -Wextra, is a strong detail.

"Is = delete a valid way to satisfy the rule?" Yes — refusing to be copied is a consistent answer, and the right one for a unique claim.

"What is worse: a class with none of the three or a class with two?" Two, because the failure moves to assignment only and the present functions suggest the question was considered. This is the question that separates having read the rule from having used it.

"What did C++11 change?" Two more functions, and suppression rules — the Rule of Five.

13. What You Should Be Able to Say Now

  1. Write one of the three, and you almost certainly need all three — a heuristic, not a language rule.
  2. All three answer one question: what does this object own?
  3. The chain: you wrote a destructor → it owns something → the memberwise copy shares it → the memberwise assignment shares and leaks.
  4. Measured, destructor only: exit 133, ASan double-free … in ~Buffer(), 0 compiler diagnostics.
  5. Measured, destructor + copy constructor: 3 live buffers for 2 objects, and 1 still live after both were destroyed.
  6. Measured, all three: exit 0, ASan clean, self-assignment safe.
  7. Measured in the same row: release-before-allocate with a throwing allocation gave a live count of −1.
  8. = delete is a complete answer — correct permanently for a unique claim, a placeholder for data (Chapter 12.9 §9).
  9. The compiler states the rule: definition of implicit copy constructor for 'D' is deprecated because it has a user-provided destructor.
  10. It needs -Wdeprecated-copy-dtor — not in the standing set, where the broken class gave 0 diagnostics.
  11. "Deprecated" means still generated, and still shallow.
  12. The half-repaired state is worse than the broken one — the failure narrows to assignment and the present functions discourage checking.
  13. Treat the three as one edit.
  14. The rule predates moves; declaring a destructor also suppresses the generated moves (Chapter 13.10).
  15. The right count is usually zero (Chapter 13.11).

14. Exercises

Exercise 1 — apply it. For each class, say which of the three you would write: struct P { int x, y; };, a class holding new int[n], a class holding a std::vector<int>, a class holding a FILE* it opened, a class holding a borrowed Logger*.

Exercise 2 — the chain. Write out the four-step reasoning from "I wrote a destructor" to "copy assignment is wrong twice over".

Exercise 3 — the middle row. Given a class with a destructor and a deep copy constructor, write a three-line main that leaks and a three-line main that double-frees.

Exercise 4 — which syntax? Explain why Buffer b = a; is correct and b = a; is not, for that class, and why the two are easy to confuse.

Exercise 5 — refuse instead. Rewrite a buffer-owning class using = delete, and list three things callers can no longer do.

Exercise 6 — the warning. Say what -Wdeprecated-copy-dtor reports, on which class, and why it is not on by default.

Exercise 7 — worse or better? Argue, with the measurements, that a class with two of the three is worse than one with none.

Exercise 8 — zero. Rewrite the buffer class so that the correct number of these functions is zero, and name what is doing the work.

15. Summary

  • Write one of the three and you need all three — because all three answer what does this object own?
  • The chain: a destructor implies ownership; ownership makes the memberwise copy share; assignment shares and leaks.
  • Measured: destructor only → exit 133; destructor + copy constructor → 1 buffer still live; all three → exit 0, ASan clean.
  • Measured: the wrong assignment ordering gave a live count of −1.
  • = delete is a complete answer, permanent for a unique claim.
  • The compiler states the rule — implicit copy constructor … is deprecated because it has a user-provided destructor — under -Wdeprecated-copy-dtor, which is not in the standing flags.
  • "Deprecated" means still generated and still shallow.
  • Two of three is worse than none — the failure narrows and the present functions discourage checking.
  • Treat them as one edit, and prefer zero (Chapter 13.11).

The rule is stated and it stops short of something the previous section admitted: it counts to three because it was written when there were three, and the language has had five since C++11. The two additions are not more of the same. A copy answers the question what does a duplicate of me own by making a second resource; the new pair answers a different question entirely, which is what to do when the object being copied from is one nobody will use again — in which case duplicating its resource is pure waste, and the resource could simply be handed over. Chapter 13.2 measured the cost of not having that option as 2n − 1 copies for n container insertions. Before the mechanism can be written, there has to be a way to tell which objects nobody will use again, and that is a property of expressions rather than of types.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.