Skip to content
VLSI Mentor

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

What Copying an Object Means

Four chapters have now deferred to this module. Chapter 10.8 deleted the copy operations on its buffer class and called the deletion mandatory without saying why. Chapter 12.4 explained the mechanism that makes a deletion work and said the design question belonged elsewhere. Chapter 12.7's callout observed that a hand-written destructor raises the question immediately. Chapter 12.9 named it as the one thing standing between a correct resource-owning class and a type you could actually return from a factory or put in a container. The question is what it should mean to copy an object that owns something — and the first thing to understand is that it is not an open question, because the compiler has already answered it for every type you have ever written. It generates a copy constructor whenever you do not, that generated function does something specific and predictable, and whether its answer is right depends entirely on what the members are. This chapter measures the same generated function on two types. On one it produces exactly what you would want, for a reason worth understanding rather than assuming. On the other it produces one buffer with two owners, zero compiler diagnostics, and a process killed by a signal.

Intermediate17 min readC++Part IFundamentalsCopy Semantics

Part I · Module 13 · Chapter 13.1 · What Copying an Object Means

1. The Question Is Already Answered

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Widget a;
    Widget b = a;        // what just happened?

Chapter 12.4 §1 measured that every class gets five special member functions it did not ask for, and two of them are about copying: a copy constructor and a copy assignment operator. So Widget b = a; compiles for any type you write, whether or not you thought about what it should mean.

The compiler has already decided what copying your type means. The only question is whether its decision is correct.

2. What the Generated Copy Does

The rule is short:

The generated copy constructor copies each member, using that member's own copy constructor.

It is memberwise, and it is recursive — each member is copied the way that member's type says to copy it. Nothing clever, nothing deep, no special handling for pointers.

Two consequences, and the whole module comes out of the gap between them.

3. On a Value Type: Correct

Azvya Education Pvt. Ltd.VLSI Mentor
memberwise.cpp — complete program
#include <cstdio>
#include <string>

struct Value { int a; double b; std::string s; };

int main()
{
    Value x{1, 2.5, "a long enough string to allocate past the SSO limit"};
    Value y = x;                      // the compiler's copy constructor

    std::printf("x.s data at %p\n", static_cast<const void*>(x.s.data()));
    std::printf("y.s data at %p\n", static_cast<const void*>(y.s.data()));
    std::printf("same buffer? %s\n", (x.s.data() == y.s.data()) ? "YES" : "no");

    y.a = 99; y.s[0] = 'Z';
    std::printf("after modifying y: x.a=%d y.a=%d  x.s[0]=%c y.s[0]=%c\n",
                x.a, y.a, x.s[0], y.s[0]);
    return 0;
}

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x.s data at 0x102e2d9a0
y.s data at 0x102e2d9e0
same buffer? no
after modifying y: x.a=1 y.a=99  x.s[0]=a y.s[0]=Z

Two independent objects. Different character buffers, and modifying y left x untouched in both the int and the string.

The reason is worth stating precisely, because it is the mechanism the rest of the module exploits. The generated copy did not do anything deep. It copied three members:

MemberHow it was copied
int aa plain copy of 4 bytes
double ba plain copy of 8 bytes
std::string sby calling std::string's own copy constructor

And std::string's copy constructor allocates a new buffer and copies the characters — which Chapter 9.6 §4 measured as a real heap allocation once past the inline capacity of 22 that Chapter 9.5 measured. So the deep copy happened, and somebody else wrote it.

That is the whole trick, and Chapter 13.11's Rule of Zero is its logical conclusion.

4. On a Resource Owner: Fatal

The same generated copy, on a class that holds a raw pointer.

Azvya Education Pvt. Ltd.VLSI Mentor
rawowner.cpp — complete program
#include <cstdio>

class Buffer {
public:
    explicit Buffer(int n) : size_(n), data_(new int[n])
    { for (int i = 0; i < n; ++i) data_[i] = i; std::printf("  ctor   data_=%p\n", (void*)data_); }
    ~Buffer() { std::printf("  dtor   data_=%p  delete[]\n", (void*)data_); delete[] data_; }
    int  size() const { return size_; }
    int* data() const { return data_; }
private:
    int  size_;
    int* data_;
};

int main()
{
    Buffer a(4);
    Buffer b = a;                     // the compiler's copy constructor again
    std::printf("  a.data()=%p  b.data()=%p  same? %s\n",
                (void*)a.data(), (void*)b.data(),
                (a.data() == b.data()) ? "YES -- ONE buffer, TWO owners" : "no");
    return 0;
}

Compiler diagnostics: 0. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
creating a:
  ctor   data_=0x102e6d9a0
copying a into b (compiler-generated copy):
  a.data()=0x102e6d9a0  b.data()=0x102e6d9a0  same? YES -- ONE buffer, TWO owners
leaving scope (two destructors on one buffer):
  dtor   data_=0x102e6d9a0  delete[]
  dtor   data_=0x102e6d9a0  delete[]
exit=133

Two destructors, one pointer, and exit 133 — which Chapter 10.7 §5 identified as SIGTRAP, the allocator detecting a block already freed.

Under AddressSanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: attempting double-free on 0x6020000000d0
SUMMARY: AddressSanitizer: double-free rawowner.cpp:8 in Buffer::~Buffer()

5. The Rule Did Not Change

This is the part worth sitting with, because it is tempting to read §4 as the compiler doing something wrong.

It did exactly what §2 says, both times. It copied each member using that member's own copy constructor:

MemberHow it was copied
int size_a plain copy of 4 bytes
int* data_a plain copy of 8 bytes

A pointer's copy constructor copies the pointer. That is what copying a pointer means, and it is correct — Chapter 8.1 measured a pointer as 8 bytes holding an address, and copying those 8 bytes gives you a second pointer to the same place.

The generated copy produced a second object holding the same address, because that is what copying a pointer does.

Nothing is broken in the copy. What is broken is that the class claimed ownership of what the pointer points at — it allocated in its constructor and releases in its destructor — and the generated copy knows nothing about that claim.

6. The Failure Is in the Destructor, Not the Copy

ASan's report names Buffer::~Buffer() as the site, and that is the correct diagnosis rather than an artefact of where the crash happened.

The copy itself did nothing invalid: two pointers to one buffer is a perfectly legal state, and Chapter 10.9 §5 called it borrowing. Plenty of correct programs have many pointers to one object.

What made it fatal is that both objects then behaved as owners. Each destructor released the buffer, because each object's destructor was written on the assumption that it was the only one.

So the inconsistency is between two functions:

The destructor says I own what data_ points at.

The generated copy says a copy of me is another object holding the same data_.

Both are reasonable statements. Together they are a contradiction, and Chapter 13.5's Rule of Three is exactly the observation that these functions have to agree.

7. Which Types Have This Problem?

The test is Chapter 10.9's ownership question, asked of the members:

A member that owns nothing — int, double, bool, an enum class — copies correctly by default. There is nothing to share.

A member that owns something and manages it itself — std::string, std::vector, std::unique_ptr — copies correctly by default, because its own copy constructor does the right thing. §3 measured this. unique_ptr is the interesting case: it cannot be copied, so the generated copy of a type holding one is deleted rather than wrong, which Chapter 12.4 §9 measured as implicitly-deleted.

A member that is a raw pointer, handle, or descriptor the object claims to own — §4's case. The default is wrong.

So the question for any class reduces to:

Does this type own anything through a member that does not manage itself?

If no, the generated copy is correct and you should write nothing — Chapter 13.11. If yes, you must say what copying means, and the next three chapters are how.

8. Three Outcomes, Not Two

Sections 3 and 4 suggest a binary — the default is either right or wrong. There is a third outcome, and it is the safest of the three. Measured across four types:

Typecopy-constructibleWhat that means
struct A { int x; double y; };1correct — owns nothing
struct B { std::string s; std::vector<int> v; };1correct — members manage themselves
struct C { int* p; };1compiles, and may be wrong
struct D { std::unique_ptr<int> p; };0copy is deleted — refuses

Row C is §4's situation and the dangerous one: it compiles, so nothing asks the question. Row D is the interesting one, because the compiler declined to write a copy constructor at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: call to implicitly-deleted copy constructor of 'D'
note: copy constructor of 'D' is implicitly deleted because field 'p' has a deleted copy constructor
note: copy constructor is implicitly deleted because 'unique_ptr<int>' has a user-declared move constructor

Three nested notes, tracing the deletion to its source. D cannot be copied because unique_ptr cannot be copied, and unique_ptr cannot be copied because it has a move constructor — which is a rule about special members that Chapter 13.10 owns and which is visible here as a diagnostic.

Two things worth taking from this.

The deletion propagates outward, and that is the mechanism Chapter 12.4 §9 measured for a member with no default constructor: a type is only as copyable as its least copyable member. So choosing std::unique_ptr for an owned member does not merely document the ownership — it makes the wrong copy unexpressible, which is Chapter 9.9's make an invalid state unrepresentable applied to copying.

And a compile error is the good outcome. Compare the three failure modes available to a resource-owning class: row D fails at compile time at the call site; row C fails at runtime with exit 133 and zero diagnostics. Chapter 10.7 §6 made the general argument — the quiet failure is the expensive one — and this is the same asymmetry in the copy. A type that refuses to be copied is strictly easier to work with than one that is copied wrongly, which is why Chapter 12.9 §4's Handle deleted its copy operations rather than leaving them generated.

9. What to Actually Do

Ask whether your type owns anything through a member that does not manage itself. That one question decides everything in this module.

Prefer members that manage themselves — std::string, std::vector, std::unique_ptr. Then the generated copy is correct (Chapter 13.11).

Treat a raw pointer member as a decision you have not made yet. Either it is a borrowed observer (Chapter 14.7) or you owe this module an answer.

Remember the default is memberwise and recursive — not shallow in a mysterious way, just a copy of each member.

Expect no diagnostic. Measured: 0 compiler warnings on the fatal case.

If you wrote a destructor, you have a copy question — Chapter 12.7's callout, and Chapter 13.5's rule.

10. Mistakes Worth Making Once

Assuming the compiler's copy is wrong in general. It is right for value types, and §3 measured why.

Assuming it does something deep for you. It copies each member; the depth comes from the members.

Writing a class with a raw pointer and a destructor and stopping there. Measured: one buffer, two owners, exit 133.

Expecting a warning. 0 diagnostics at the standing flags.

Reading the crash as a copy bug. ASan names the destructor, and that is the honest site — the copy was legal, the double ownership was not.

Thinking two pointers to one object is itself an error. It is borrowing (Chapter 10.9 §5); the error is two owners.

Forgetting that std::unique_ptr makes the copy deleted rather than wrong. Chapter 12.4 §9's implicitly-deleted.

11. Interview Relevance

"What does the compiler-generated copy constructor do?" Copies each member using that member's own copy constructor — memberwise and recursive. Candidates who say "a shallow copy" are close but miss the point that the depth is entirely the members' business.

"When is it wrong?" When a member is a raw pointer (or handle) that the class claims to own, because the copy duplicates the pointer and both destructors then release the same resource. The measured exit=133 and the ASan double-free naming the destructor make this concrete.

"Why is it right for a type holding a std::string?" Because std::string's own copy constructor allocates and copies — the deep copy is done by the member, not by your type.

"Is two pointers to one object a bug?" No — two owners is. This distinction is the useful one.

"What are your options when the default is wrong?" Deep copy, forbid copying, move instead of copying, or redesign so every member manages itself — and the last is the one to prefer.

12. What You Should Be Able to Say Now

  1. The compiler has already decided what copying your type means — it writes a copy constructor when you do not (Chapter 12.4 §1).
  2. The generated copy is memberwise and recursive: each member copied by its own copy constructor.
  3. Measured on a value type: different string buffers (…9a0 vs …9e0), fully independent after modification.
  4. The deep copy there was done by std::string's own copy constructor, which allocates past the 22-character inline capacity (Chapters 9.5, 9.6).
  5. Measured on a raw-pointer owner: a.data() == b.data() — one buffer, two owners — with 0 compiler diagnostics.
  6. Then two destructors on one pointer and exit=133 (SIGTRAP), and ASan reported double-free … in Buffer::~Buffer().
  7. The rule did not change — a pointer's copy constructor copies the pointer, which is correct.
  8. The failure is a contradiction between two functions: the destructor claims ownership, the generated copy produces a second claimant.
  9. ASan names the destructor, which is the right diagnosis.
  10. Two pointers to one object is borrowing (Chapter 10.9 §5); two owners is the bug.
  11. The test: does the type own anything through a member that does not manage itself?
  12. Three answers — deep copy, forbid, or move — and a fourth to prefer: hold members that manage themselves (Chapter 13.11).

13. Exercises

Exercise 1 — predict. For each type, say whether the generated copy is correct: struct P { int x, y; };, struct Q { std::string s; };, struct R { int* p; };, struct S { std::vector<int> v; };, struct T { std::unique_ptr<int> p; };

Exercise 2 — trace it. For §4's Buffer, write the sequence of events from Buffer b = a; to process exit, and name the exit code.

Exercise 3 — why the string works. Explain, in terms of §2's rule, why struct Value copies correctly without anyone writing a copy constructor for it.

Exercise 4 — the contradiction. State the two statements that conflict in §4's Buffer, and say which one you would change.

Exercise 5 — borrowing or owning? For each, say whether a second pointer is a problem: a const char* parameter, a cached pointer in an observer list, a data_ member released in a destructor, a std::string_view.

Exercise 6 — no diagnostics. A colleague argues their class is fine because it compiles with -Wall -Wextra. Give the measurement that rebuts this.

Exercise 7 — the fourth answer. Rewrite §4's Buffer so that no copy constructor is needed at all, and say which chapter formalises what you did.

14. Summary

  • The compiler already decided what copying your type means; the question is whether it is right.
  • The generated copy is memberwise and recursive — each member by its own copy constructor.
  • Measured on a value type: independent buffers, independent values — correct, because std::string did the deep copy.
  • Measured on a raw-pointer owner: one buffer, two owners, 0 diagnostics, then two destructors and exit 133; ASan: double-free … in ~Buffer().
  • The rule never changed — copying a pointer copies the pointer.
  • The failure is a contradiction between a destructor that claims ownership and a copy that creates a second claimant.
  • Two pointers is borrowing; two owners is the bug.
  • The test: does the type own anything through a member that does not manage itself?
  • Three answers — deep copy, forbid, move — and a fourth to prefer: self-managing members.

The question is now precise and the answer is a function that does not exist yet. Section 4's Buffer needs a copy constructor of its own — one that allocates its own storage instead of taking the address of someone else's — and writing it is the straightforward half of this module. The less obvious half is knowing when it runs, because a copy constructor is called in places where nothing in the source says copy: passing an argument by value, inserting into a container, and in one case measured in the next chapter, six times for three insertions, because a container that runs out of room copies everything it already holds into the new space.

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.