Skip to content
VLSI Mentor

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

Copy Assignment

A copy constructor builds a new object out of an existing one, which means it begins from nothing and has nothing to clean up. The other half of copying has none of that simplicity. When the destination already exists it is already holding a resource of its own, so taking on a copy of the source's means three things rather than one: disposing of what it had, acquiring and filling storage for the new contents, and doing those in an order that does not destroy the thing being copied from. The last of those turns out to matter in a case that looks too silly to happen and happens regularly in real code — when the source and the destination are the same object, which is easy to arrange through two references or two pointers without anyone writing anything that looks like it. This chapter measures two implementations that a careful person might write, and they fail differently. One of them produces a silently wrong value and exits zero with AddressSanitizer reporting nothing at all. The other is a genuine read of freed memory that the sanitizer catches immediately. The standard account of this bug describes only the second, and getting the distinction right turns out to matter for how you check your own code.

Intermediate18 min readC++Part IFundamentalsCopy Semantics

Part I · Module 13 · Chapter 13.3 · Copy Assignment

1. The Signature

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

Three parts, each with a reason:

operator= — assignment is an operator, and this is a member function implementing it. Chapter 15.6 owns the general rules for overloading it; this chapter writes the one case that matters for resource ownership.

const Buffer& parameter — the same reasoning as Chapter 13.2 §1: a reference so there is no copy to make the copy, const so a const source works.

Buffer& return type, returning *this. That is what makes a = b = c work, and Chapter 11.6 §5 established *this as the object itself. Returning by value would copy; returning void would work for most code and breaks chaining, so the convention is universal.

2. Why It Is Harder Than the Constructor

Copy constructorCopy assignment
The destinationdoes not exist yetalready exists
It currently ownsnothinga resource
So it mustallocate and fillrelease, allocate, fill
And it must survive—being handed itself

The extra work is the whole chapter. Chapter 13.2's version was two steps; this needs three, in an order that is not obvious.

3. The Obvious Version

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Naive& operator=(const Naive& o)
    {
        delete[] data_;                   // release what we had
        size_ = o.size_;
        data_ = new int[o.size_];
        std::copy(o.data_, o.data_ + o.size_, data_);
        return *this;
    }

Read it as ordinary code and it is unobjectionable: release the old buffer, get a new one the right size, copy the contents in. For a = b with two distinct objects it is correct.

4. Naive Shape One: Silent Garbage

Now a = a. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compiler: warning: explicitly assigning value of variable of type 'Naive' to itself [-Wself-assign-overloaded]
before: a.first()=1 size=4
doing a = a;
after : a.first()=0 size=4
exit=0

The value went from 1 to 0, the program exited 0, and AddressSanitizer reported nothing at all.

The mechanism is worth tracing precisely, because it is not the use-after-free the textbooks describe:

  1. delete[] data_ releases the buffer.
  2. size_ = o.size_ — o is *this, so this assigns size_ to itself. Harmless.
  3. data_ = new int[o.size_] — a fresh, uninitialized buffer.
  4. std::copy(o.data_, o.data_ + o.size_, data_) — and o.data_ is now the pointer we just assigned in step 3.

So step 4 copies the new uninitialized buffer onto itself. No freed memory is read, which is why ASan is silent; the object simply ends up holding whatever new happened to return. Measured as 0.

The compiler does warn here, but only for the literal form. -Wself-assign-overloaded catches a = a and says nothing about this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Naive* p = &a;
    *p = *p;              // no warning

Which is the realistic shape: self-assignment in real code arrives through two references, two pointers, or two iterators into the same container, never as a literal a = a.

5. Naive Shape Two: A Real Use-After-Free

A second reasonable way to write the same function — capture the source first, then release:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Naive2& operator=(const Naive2& o)
    {
        const int  n   = o.size_;
        const int* src = o.data_;        // capture the source buffer
        delete[] data_;                  // for self-assignment this frees src
        size_ = n;
        data_ = new int[n];
        std::copy(src, src + n, data_);  // READS FREED MEMORY when &o == this
        return *this;
    }

Measured, with the self-assignment arriving through a pointer so the compiler cannot see it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
diagnostics: 0
before: a.first()=1
after : a.first()=0
exit=0
ASan: ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000d0
      SUMMARY: AddressSanitizer: heap-use-after-free ... in __constexpr_memmove<int, int const>

This one is a genuine use-after-free, and ASan catches it — reporting from inside the library's copy routine, which is the same shape as Chapter 9.4's unterminated-string report coming from inside strlen.

Note that the plain run is indistinguishable from §4: value 1 became 0, exit 0, zero diagnostics. Two different bugs with identical symptoms outside a sanitizer.

6. Why the Distinction Matters

The textbook account of this hazard says naive copy assignment causes a use-after-free on self-assignment. Section 5 is that case. Section 4 is not, and the difference has a practical consequence:

A sanitizer-clean run does not prove your copy assignment handles self-assignment.

If your implementation has §4's shape, ASan will never mention it no matter how many self-assignments your tests perform, because nothing invalid is accessed — the object is just wrong. Chapter 10.7 §6 reached the same conclusion about silent failures being the expensive ones, and Chapter 12.9 §7 found no compiler flag at any level reporting a skipped release. This is the third instance in this curriculum of a bug that tooling cannot see.

So the check is not "does the sanitizer pass". It is reading the function and asking: what does this do when &o == this?

7. The Correct Version

Azvya Education Pvt. Ltd.VLSI Mentor
guard.cpp — copy assignment done properly
    Safe& operator=(const Safe& o)
    {
        if (this == &o) return *this;               // 1. the guard

        int* fresh = new int[o.size_];              // 2. allocate FIRST
        std::copy(o.data_, o.data_ + o.size_, fresh);

        delete[] data_;                             // 3. only then release
        data_ = fresh;
        size_ = o.size_;
        return *this;
    }

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a.first()=10  b.first()=70
  guard: self-assignment, returning early
after self-assignment: a.first()=10
after a = b:           a.first()=70
ASan: clean

Self-assignment left the value at 10; a real assignment produced 70. Three decisions in that function:

The guard — if (this == &o) return *this; This is Chapter 11.6 §5's identity comparison, and it makes self-assignment a no-op. It is one line and it closes both §4 and §5.

Allocate before releasing. This is the more important of the two, because it also fixes something the guard does not: if new throws, the old buffer is still intact and the object is unchanged. The §3 ordering would have released the buffer and then thrown, leaving the object holding a dangling pointer. Chapter 22.5's exception-safety guarantees are this property named properly.

Then release and commit. The three assignments at the end cannot fail, so once the allocation has succeeded the operation cannot leave the object half-updated — which is Chapter 11.5 §7's requirement that the invariant hold whenever control leaves the type's code.

8. The Other Two Failures, Measured

Sections 4 and 5 covered self-assignment. The ordering in §7 was justified by two further claims — that forgetting to release leaks, and that allocating first survives a failed allocation — and both are worth measuring rather than asserting. Three variants, with a counter tracking live buffers:

A. Forgetting to release the old buffer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
live buffers while both objects exist: 3  (expected 2)
live buffers after both destroyed: 1  <-- leaked

Two objects, three buffers. After both destructors ran, one buffer was still live — Chapter 10.6's leak, arriving through assignment rather than through a missing delete. Note it is not detected by anything: the program is otherwise correct.

B. Release first, then the allocation throws.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
caught bad_alloc
a.raw() unchanged? yes -- but it is DANGLING   live=1
live after scope: -1

Two things went wrong. The object's pointer is unchanged and dangling — it still holds the address of a buffer that was released before the throw, so any later use is Chapter 10.7's use-after-free. And then the destructor released it again, which the counter records as −1: more releases than acquisitions.

That negative number is the double release made arithmetic. In a real program it is Chapter 10.7 §5's exit 133.

C. Allocate first.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
caught bad_alloc
a.raw() unchanged? yes -- and still VALID   live=2
live after scope: 0

The allocation threw before anything was modified, so the object still holds its original, valid buffer, and the counter returns to 0 at scope exit. The assignment did not happen; nothing else changed either.

Measured: the same failure produced a dangling pointer and a double release in one ordering, and no observable effect at all in the other.

This is the property Chapter 22.5 calls the strong exception guarantee — an operation either succeeds or leaves the object exactly as it was — and §7's ordering is what buys it. It is also the reason allocate-before-release matters more than the self-assignment guard: the guard fixes one input, and the ordering fixes every path on which the allocation can fail.

9. The Guard Is Not the Whole Answer

A subtlety worth having, because "add a self-assignment check" is the usual advice and it is incomplete.

The guard handles the case where the two objects are identical. It does not handle the case where they overlap — two objects sharing part of a resource — which is rarer and harder, and which is one more reason Chapter 13.11's advice to hold self-managing members is the real solution.

More usefully, there is an idiom that solves all three of §2's problems without a guard at all: build a complete copy into a local object, then exchange it with *this, letting the local's destructor release the old contents on the way out. It is called copy-and-swap, it is naturally exception-safe for the reason given in §7, and it needs std::swap, which belongs to Module 21. It is named here so you recognise it in real code; the explicit version above is the one to understand first because it makes the three steps visible.

10. What Happens When Both Objects Own Something

One more measured fact from §7's output, easy to miss: after a = b, a's original buffer was released and a holds a copy of b's contents — a.first() became 70. b is untouched.

That is the definition of copy assignment and it is worth stating because the next half of this module changes it. Chapter 13.8's move assignment solves the same three problems of §2 by taking the source's resource instead of duplicating it, which is cheaper and leaves the source empty rather than untouched. Same function shape, same three problems, different resolution.

11. What to Actually Do

Return Buffer& and return *this so chaining works.

Take const Buffer&.

Guard against self-assignment — if (this == &o) return *this;.

Allocate before releasing, so a failed allocation leaves the object unchanged.

Release, then commit with operations that cannot fail.

Do not rely on a sanitizer to find self-assignment bugs — §4's shape is invisible to it.

Read the function asking "what if &o == this?" rather than testing for it.

Know copy-and-swap exists (§8) and understand the explicit version first.

And prefer members that manage themselves, so the generated assignment is correct (Chapter 13.11).

12. Mistakes Worth Making Once

Releasing before allocating. Two failures at once: self-assignment, and a dangling pointer if new throws.

Omitting the self-assignment guard. Measured: 1 became 0, exit 0.

Assuming self-assignment is always a use-after-free. Measured: one shape is, one shape silently produces garbage with ASan clean.

Relying on -Wself-assign-overloaded. It catches a = a and not *p = *p.

Testing under a sanitizer and concluding correctness. §4 produces no report.

Forgetting to release the old resource at all. Chapter 10.6's leak.

Returning void or by value. The first breaks chaining; the second copies.

Writing this function for a class holding a std::vector. It is already correct.

13. Interview Relevance

"Write copy assignment for a class owning a buffer." The three things being looked for are the self-assignment guard, allocate-before-release, and returning *this. Candidates who release first have written the common bug.

"What goes wrong on self-assignment?" The strong answer distinguishes the two shapes: release-then-allocate copies a fresh uninitialized buffer onto itself and is silent, while capture-then-release genuinely reads freed memory. Saying "it's a use-after-free" is the textbook answer and only half right.

"Why allocate before releasing?" Self-assignment, and exception safety — if the allocation throws, the object still holds its original resource. The second reason is the one that matters more and is mentioned less.

"Why return a reference?" To support a = b = c, and to avoid a copy.

"Would a sanitizer find a missing self-assignment guard?" Not necessarily — and that is a genuinely discriminating question.

"When would you not write this function?" When the members manage themselves, which is almost always. The Rule of Zero.

14. What You Should Be Able to Say Now

  1. Buffer& operator=(const Buffer&), returning *this — reference return for chaining, const& parameter for the same reasons as the copy constructor.
  2. It is harder than the constructor because the destination already owns something and must survive being handed itself.
  3. Measured, naive shape one (release first): a = a turned 1 into 0, exit 0, and ASan reported nothing.
  4. The mechanism: after reallocating, o.data_ is the new pointer, so the copy copies a fresh uninitialized buffer onto itself.
  5. Measured, naive shape two (capture then release): a genuine heap-use-after-free, caught by ASan from inside the library's copy routine.
  6. Both plain runs look identical — 1 to 0, exit 0, no diagnostics.
  7. -Wself-assign-overloaded catches only the literal a = a; *p = *p draws nothing.
  8. So a clean sanitizer run does not prove self-assignment is handled — the third such case in this curriculum.
  9. The correct order: guard, allocate, copy, release, commit. Measured: self-assignment left 10, a = b gave 70, ASan clean.
  10. Allocate-before-release also gives exception safety — a failed new leaves the object unchanged (Chapter 22.5).
  11. The guard handles identity, not overlap.
  12. Copy-and-swap solves all three problems at once and needs std::swap (Module 21).
  13. Copy assignment duplicates and leaves the source untouched; Chapter 13.8's move takes instead.
  14. Prefer not to write it — a std::vector member makes the generated one correct.

15. Exercises

Exercise 1 — write it. Give copy assignment for a class holding char* text_ and int length_, in the §7 order, and mark which step can fail.

Exercise 2 — trace the naive version. For §3's implementation and a = a with size_ == 4, write what happens at each of the four steps and say what the object ends up holding.

Exercise 3 — which shape? Classify each as silent-garbage or use-after-free: (a) release, allocate, copy from o.data_ (b) capture o.data_, release, allocate, copy from the captured pointer (c) guard, allocate, copy, release.

Exercise 4 — the invisible one. Explain why a sanitizer finds (b) and not (a), and what that means for how you check your code.

Exercise 5 — exception safety. Show the state the object is left in if new throws, for the §3 order and the §7 order.

Exercise 6 — the realistic self-assignment. Give three ways *p = *q can turn out to be self-assignment without anyone writing a = a.

Exercise 7 — why *this? Explain what breaks if copy assignment returns void, and what breaks if it returns by value.

Exercise 8 — delete the function. Rewrite the class so copy assignment need not be written, and name what is doing the work instead.

16. Summary

  • Buffer& operator=(const Buffer&) returning *this.
  • Harder than the constructor: the destination already owns something and may be the source.
  • Measured, release-first: a = a gave 1 → 0, exit 0, ASan silent — it copies a fresh uninitialized buffer onto itself.
  • Measured, capture-then-release: a genuine heap-use-after-free.
  • Both look identical in a plain run, and -Wself-assign-overloaded catches only the literal a = a.
  • So a clean sanitizer proves nothing here.
  • The correct order: guard, allocate, copy, release, commit — measured working, ASan clean.
  • Allocate-before-release is also exception safety (Chapter 22.5).
  • Copy-and-swap does all three at once (Module 21's std::swap).
  • Prefer members that manage themselves and write none of it.

Both halves of copying are now written, and the module has described the failure they repair without ever drawing it. That is a gap worth closing, because the bug has a shape: two objects, each believing it owns a buffer, and one buffer. Everything that goes wrong follows from that picture, including why the crash happens in the destructor rather than at the copy, and why the repair is not to make the copy cleverer but to make there be two buffers. The next chapter draws it, traces the exact sequence of events from the copy to the signal, and puts the deep-copy version beside it so the difference is one arrow instead of two.

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.