Class design best practices
When you write a class, you decide what happens when an object is created, copied, moved, and destroyed.
Why Every Value Movement Matters
When you write a class, you decide what happens when an object is created, copied, moved, and destroyed. These operations are the special member functions of the class. Each time a value is passed around in a program, one of these operations may run, often without the programmer noticing. A function call that takes an object as an argument, a function that returns an object, and an assignment that stores one object into another can each trigger copying or moving. Class design is the discipline of deciding, in advance, what those operations should do for your type.
This article follows one central idea: copying is not automatically correct for every type. A class author has to decide whether the default copying behavior suits the type and, if it does not, change it. The discussion covers three situations: prohibiting or redefining copying, returning containers by value, and designing the constructors and destructor as a set. Before reading the explanation, try to predict what a default copy does when a class holds a pointer to a resource.
What do you think happens?
A class stores a pointer to a heap buffer. You create object a, then create object b as a default copy of a. After the copy, what does b's pointer point to, and what happens when both objects are eventually destroyed?
Reveal answer
b's pointer holds the same address as a's pointer, so both objects share one buffer.
The default copy duplicates each member's value. For a pointer member, the value copied is the address, so both objects end up referring to the same resource. Nothing new is allocated. When the first object releases the buffer in its destructor, the second object is left pointing at memory that no longer belongs to it, and releasing it again from the second object is an error.
When Default Copying Goes Wrong
The compiler provides default copy behavior for a class unless you say otherwise. For many types this is exactly right: each member is copied, and the result is a valid, independent object. The default behavior becomes a problem when a member refers to a resource outside the object itself. A pointer member is the common case. The default copy duplicates the pointer value, not the thing it points to. The two objects then share one resource, and the class has no way of knowing that two objects depend on it.
The diagram shows the situation from the prediction. Object b received a copy of the pointer value, so both arrows lead to the same buffer. Two independent objects now depend on one resource. If the first object is destroyed and releases the buffer, the second object still holds a pointer to memory that has been given back. Using or releasing that memory through the second object is wrong, and that is why copying must be examined for any type that owns a resource.
- data_ is a raw pointer, so its value is what gets copied
- The destructor releases the array that data_ refers to
- The copy line creates two objects that refer to one array
Redefining or Prohibiting Copying
Once you recognize that default copying does not suit a type, you have two choices. You can redefine copying, so that the copy operation produces a correct, independent object, typically by allocating a new resource and copying its contents. Or you can prohibit copying altogether, so that code which tries to copy the object fails to compile. The source describes both options as parts of one decision: the class author decides what copying means for the type, and changes the default when it does not fit.
Prohibiting is the right choice when an object represents something that should exist only once, or when a sensible duplicate cannot be defined. Redefining is the right choice when a duplicate is meaningful and can be made independent. In both cases the decision belongs to the class, not to the caller who happens to write an assignment.
- = delete on the copy constructor prohibits copy construction
- = delete on the copy assignment prohibits copy assignment
- The destructor still releases the resource exactly once
Returning Containers by Value
A function can return a container by value, relying on move operations for efficiency. Instead of forcing the caller to pass in an output object for the function to fill, the function builds its result locally and returns it. This style makes the function's interface simple: the result comes out of the function, and the caller receives it directly.
Returning by value would be expensive if the returned container had to be copied in full, because a container's contents can be large. The move operation solves this. It is what allows the contents of the returned object to be handed to the caller efficiently, rather than copied. The caller then owns the contents, and the local object is left in a state where it no longer holds them.
The before and after states show the transfer. Before the return, the local container owns the buffer. After the move, the caller's variable owns it, and the local object has been left empty of that buffer. No element of the buffer had to be copied one by one to make this happen. This is what the source means by relying on move for efficiency.
- The move constructor takes the pointer instead of copying the array
- Setting other.data_ to nullptr leaves the local object with nothing to release
- Returning local lets the caller receive the buffer without a full copy
Tracing a Returned Buffer
A function builds a buffer of 1000 elements inside itself and returns it. Trace which object owns the buffer at each stage.
- 1Inside the function
The local object is constructed and allocates the buffer. The local object owns the buffer.
- 2At the return statement
The local object is about to leave scope, so the return hands it to the caller. The move operation transfers the buffer pointer to the caller's object.
- 3After the move
The caller's object owns the buffer. The local object's pointer has been set to nothing, so its destructor has nothing to release.
- 4After the caller finishes
The caller's object is destroyed later, and at that point it releases the buffer exactly once.
The buffer was transferred, not copied, and was released by exactly one owner.
Passing Large Operands as Const References
Returning by value is one half of the advice. The complementary half concerns inputs. When an operand is large, the source recommends taking it as a const reference argument. A const reference lets a function read a large object without making its own copy of it. The callee receives a reference to the caller's original object, so no copy is made on the way in.
The two rules work together. Return by value when moving is efficient, so that results come out without copying. Accept large inputs by const reference, so that they go in without copying. The const part matters too: it promises that the function will not change the caller's object, so the function can only read it.
- const Buffer& names the caller's object, not a duplicate
- Passing Buffer by value would make a copy of the whole object
- The const keyword prevents the function from changing the caller's object
What a Declared Destructor Changes
The source gives a rule: a class with a destructor probably needs user-defined or deleted copy and move operations. The reason is that a destructor usually exists because the class releases some resource. If the class releases a resource, the default copy and move behavior is suspect, as the earlier buffer example showed. Declaring a destructor is therefore a signal that the other special member functions need deliberate attention.
The rule also has a language-level consequence. In C++, declaring a destructor stops the compiler from generating the move operations, while copy operations are still generated. A class that declares a destructor and relies on the compiler for moves therefore falls back to copying in places where a move was intended. The comparison below shows the difference.
Read the two columns as a warning. In the right column, the copy operations are still present even though the class now owns a resource and the move operations are gone. This is why the rule asks you to write the copy and move operations yourself, or to delete them, rather than leaving the compiler's choices in place.
Designing the Operations as a Matched Set
The source asks you to design constructors, assignments, and the destructor as a matched set of operations. The phrase matched set means these functions should be designed together so that they agree about how an object is created, duplicated, transferred, and cleaned up. Each one is a part of the same ownership story. The constructor acquires whatever the object needs, the destructor releases it, and the copy and move operations decide what happens when that ownership is duplicated or transferred.
A design where one of these operations behaves differently from the others is hard to reason about. If the constructor allocates a resource that the destructor releases, but the copy operation shares it without any rule, the object's behavior depends on which function ran last. Consistency is the goal: when you read one operation, the others should follow the same model.
- Constructor: establishes the resource the object will own
- Copy constructor and copy assignment: decide how a duplicate is made, or prohibit it
- Move constructor and move assignment: decide how ownership is transferred cheaply
- Destructor: releases whatever the object owns
- Let the compiler generate the operations that are appropriate; write or delete the rest
Decide the ownership model for the type first, then choose which special member functions to write, delete, or leave to the compiler. A class that owns nothing outside itself usually needs no custom operations. A class that owns a resource needs all of them to agree.
Mistakes Class Authors Make
Common mistake Assuming the default copy is always correct
What it looks likeA class holding a raw pointer is copied with a plain assignment, and both objects release the same resource.
Why it failsThe default copy duplicates the pointer value, not the resource it refers to.
Do this insteadExamine copying for every type that owns a resource, then redefine or delete it.
Common mistake Writing a destructor and leaving the other operations alone
What it looks likeA class releases a buffer in its destructor but never mentions copying or moving.
Why it failsA destructor signals that the class owns a resource, and the generated copy and move operations may not handle that resource correctly.
Do this insteadDefine or delete the copy and move operations along with the destructor.
Common mistake Passing large objects by value out of habit
What it looks likeA function takes a large container as a by-value parameter, so every call copies the whole container.
Why it failsThe copy is needed only if the function must own its own object.
Do this insteadTake large read-only operands as const references.
Common mistake Forcing callers to pass an output object to avoid copies
Why it failsReturning a container by value can rely on move operations, so the output object is often unnecessary.
Do this insteadBuild the result locally and return it by value.
Common mistake Mixing behaviors across constructor, assignment, and destructor so that the operations disagree about ownership
Applying the Rules to a New Class
A class stores a pointer to a dynamically allocated array and releases the array in its destructor. Decide which of the following you would do, and justify each choice: (1) leave copying to the compiler, (2) delete copy operations, (3) write a copy that makes a new array, (4) support moves so a function can return the object by value. Then state which special member functions the class needs as a set.
Hints
- Start by asking whether the default copy would share the array between two objects.
- A destructor that releases the array tells you the class owns a resource.
- If the object should exist only once, prohibiting copying may be the right choice; if duplicates are meaningful, redefine copying.
- Remember that moves transfer the resource and leave the source with nothing to release.
Key Takeaways
- Copying is not automatically correct for every type: when the default copy duplicates a pointer to a resource, the two objects share that resource and the class must redefine or prohibit copying.
- Return containers by value and rely on move operations to transfer their contents efficiently, and accept large read-only operands as const references so they are not copied.
- A class with a destructor probably needs user-defined or deleted copy and move operations, because the destructor signals that the class owns a resource.
- Design constructors, assignments, and the destructor as a matched set so they agree about creation, duplication, transfer, and cleanup, and let the compiler generate the ones that are appropriate.