Defining Custom Copy Semantics
Default memberwise copy duplicates each member of a class, which is correct for plain values but problematic for pointers.
When a Copy Looks Simpler Than It Is
Copying an object usually feels like the simplest operation in a program. You write one line that gives a new name to a value, and you expect two independent things to exist afterward. For many ordinary types that expectation is correct. For one important family of classes it is not, and the failure is quiet: the program compiles, runs, and produces wrong results only when one of the two objects is changed later. This article is about the gap between what a copy looks like and what it actually does.
The running scenario comes from a Vector class whose objects own a collection of elements. Assigning one Vector to another is written as a single line, and the reader's first instinct is to assume the second Vector receives its own elements. The rest of this article tests that instinct, because the language's default behavior does not guarantee it.
What do you think happens?
A function receives a Vector named v1 that has four elements. Inside it, a second Vector named v2 is initialized from v1 by the default copy. Then the element at index 0 of v1 is set to 2. What does the element at index 0 of v2 hold afterward?
Reveal answer
2, the value just written through v1
Default copy gives v2 a pointer with the same value as v1's pointer, so both objects lead to one set of four elements. Writing through v1 changes the set that v2 also reads.
Default Memberwise Copy
Default memberwise copy duplicates each member of a class, one at a time. For a class whose members hold plain values, that is exactly what you want. The trouble begins when a member is not the data itself but a pointer that leads to data stored elsewhere. Copying the pointer copies the route to the data, not the data.
The misconception to clear up is that a class's default copy behavior is correct for that class. It is correct for classes whose members are plain values, such as a number or a flag. It is not automatically correct for a class whose object reaches its data through a pointer. Once a pointer is copied, two objects hold the same route, and nothing in the default copy says which object is responsible for the data at the end of that route.
Tracing the Shared Elements
Trace the scenario one step at a time. Each Vector object holds a pointer to its elements, and the sz member records how many there are. Before the copy, v1 leads to a set of four elements. The default copy then initializes v2 member by member. Its pointer receives the same value that v1's pointer has, so v2 now leads to the same set of four elements. No new elements are created.
The diagram shows the state the copy produces. Two objects exist, but only one array of elements exists. Each object's elem member is a copy of the same pointer value, so there is one resource with two routes into it.
Why the Handle's Invariant Breaks
A handle carries an invariant, which is a property it is supposed to keep true at all times. For a resource handle, the invariant concerns ownership of the resource. Default memberwise copy can break that invariant, because after the copy the program has two handles and only one arrangement of storage, with neither handle clearly owning it alone. That is the precise sense in which the default behavior is a disaster for this kind of class.
A resource handle is responsible for an object reached through a pointer. Default copying leaves two handles sharing one resource, and that sharing is what breaks the invariant. The fix is not to tweak the result of the copy after the fact. The copy behavior of such a class must be defined deliberately, rather than left to whatever the default produces.
A Destructor as a Warning Sign
A destructor is the place where a class cleans up the resource it owns when an object goes away. If a class needs to release something at the end of its life, it is almost always because it owns that something. Ownership is exactly what default copying muddles, since two objects end up pointing at one resource. So when a class has a destructor, a programmer should treat the default copy behavior with suspicion.
The source makes the same point from the compiler's side: the presence of a destructor is a strong hint that default memberwise copy semantics is wrong, and the compiler should at least warn about an example like this one. A destructor does not prove a bug, but it is the signal to review copy behavior on purpose.
Where Copy Behavior Is Declared
When you copy an object of a class, the language needs to know exactly how that copy should be produced. The answer is not a single rule. Copying an object of a class is defined by two members working together: a copy constructor and a copy assignment. Together these two members form the definition of copying for that class, and they are the place to look whenever you want to understand what a copy of one of your objects will actually contain.
- elem and sz are the private data; elem leads to resource stored outside the object's own fields
- The two copy members are the pair that defines copying for Vector
- The destructor releases the array that elem points to
The declaration above is abridged to the parts the source names: the private data, the copy members, and the destructor. The source presents the whole class as a list of responsibilities, with the copy members as the two lines that define copying.
Locating the Copy Definition When Tracing a Problem
Suppose a program behaves unexpectedly with Vector objects after one of them is copied. A practical first step is to ask which declaration governs the copy. Because copying is defined by the copy constructor and copy assignment, check those two declarations in the Vector class and confirm which member the copying operation would use. The rest of the class, such as the element access functions, does not decide how the copy is produced.
- 1Identify which operation copies the object: creation from an existing object, or overwriting an existing object.
- 2Find the matching member in the class declaration: the copy constructor for creation, the copy assignment operator for overwriting.
- 3Check whether that member copies only the pointer or also handles the resource the pointer leads to.
- 4Compare with the destructor, which shows what resource the object is responsible for releasing.
Common Mistakes When Reasoning About Copies
Common mistake Assuming a copy always creates independent data
What it looks likeAfter v2 is initialized from v1, writing v1[0] = 2 and then reading v2[0] is expected to return the old value.
Why it failsDefault memberwise copy copies the pointer value, so both objects lead to the same set of elements.
Do this insteadAsk whether any member is a pointer to data stored elsewhere. If so, the default copy shares that data.
Common mistake Treating the destructor as unrelated to copying
Why it failsA destructor shows the class owns a resource, and ownership is what default copying muddles.
Do this insteadWhen a class has a destructor, review its copy behavior before relying on the default.
Common mistake Looking only at the element access functions when a copy goes wrong
Common mistake Confusing the copy constructor with the copy assignment operator
Why it failsThe constructor runs when a new object is created, and the assignment operator runs when an existing object is overwritten.
Do this insteadCheck which situation occurred, then read the matching declaration.
Trace a Copy Before You Trust It
A class holds a pointer to an array of doubles and releases it in its destructor. Starting from a Vector v1 with four elements, a second Vector v2 is created from v1 with no custom copy members. Name the exact step where v2 starts sharing elements with v1, and state which member of the class would have to change to prevent it.
Hints
- Ask what the default copy actually copies for a member that is a pointer.
- Look for the member that defines the copy when a new object is created from an existing one.
- Your answer should name a copy constructor or copy assignment operator that copies the elements rather than the pointer.
Key Takeaways
- Default memberwise copy duplicates each member, which is correct for plain values but leaves two objects sharing one resource when a member is a pointer.
- The exact sharing step is the copy of the pointer value: the elements are not duplicated, so every later read and write hits the same array.
- A destructor signals that a class owns a resource and that its default copy semantics should be reviewed.
- Copying of a class is defined by two members, the copy constructor and the copy assignment operator, which are the first place to look when tracing a copy problem.