Concepts / Class design best practices

Class design best practices

When you write a class, you decide what happens when an object is created, copied, moved, and destroyed.

  • Programming

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?

  • b gets its own new buffer with the same contents

  • b's pointer holds the same address as a's pointer, so both objects share one buffer

  • b is left empty and has no buffer at all

Reveal answer
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.

object apointer memberheap bufferone shared resourceobject b (defaultcopy)pointer memberpoints topoints to
After a default copy, what do the two objects' pointer members point to, and what goes wrong when the first object is destroyed?

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.

cpp
Notes on the code
  • 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.

cpp
Notes on the code
  • = 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.

local containerowns the bufferlocal containerno buffer leftcaller's variablenot yet assignedcaller's variableowns the buffer
When a function returns a local container, which object owns the heap buffer before and after the return, and what is left in the local object?

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.

cpp
Notes on the code
  • 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
Worked example

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.

  1. 1
    Inside the function

    The local object is constructed and allocates the buffer. The local object owns the buffer.

  2. 2
    At 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.

  3. 3
    After 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.

  4. 4
    After the caller finishes

    The caller's object is destroyed later, and at that point it releases the buffer exactly once.

Result

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.

cpp
Notes on the code
  • 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.

copy operationsgeneratedcopy operationsstill generatedmove operationsgeneratedmove operationsnot generated
If I declare a destructor, which special member functions does the compiler still generate, and which ones does it stop generating?

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
Best practice

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 like

    A class holding a raw pointer is copied with a plain assignment, and both objects release the same resource.

    Why it fails

    The default copy duplicates the pointer value, not the resource it refers to.

    Do this instead

    Examine 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 like

    A class releases a buffer in its destructor but never mentions copying or moving.

    Why it fails

    A destructor signals that the class owns a resource, and the generated copy and move operations may not handle that resource correctly.

    Do this instead

    Define or delete the copy and move operations along with the destructor.

  • Common mistake

    Passing large objects by value out of habit

    What it looks like

    A function takes a large container as a by-value parameter, so every call copies the whole container.

    Why it fails

    The copy is needed only if the function must own its own object.

    Do this instead

    Take large read-only operands as const references.

  • Common mistake

    Forcing callers to pass an output object to avoid copies

    Why it fails

    Returning a container by value can rely on move operations, so the output object is often unnecessary.

    Do this instead

    Build 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

MEDIUM

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.

Related concepts

More concepts starting with C