Concepts / Suppressing Default Operations in C++

Suppressing Default Operations in C++

Default copy and move operations are problematic in a class hierarchy because the base class does not know about the members of the derived class.

  • Programming

Why Default Copies Look Harmless

When you define a class, the language is willing to supply copy and move operations for you. For a simple, self-contained class, letting those defaults do the work is often reasonable. Inside a class hierarchy, however, the same shortcut can quietly go wrong. This article follows one object through a base-class pointer, watches what the copy can and cannot reach, and then decides what the usual remedy should be.

Tracing a Derived Object Through a Base Pointer

The key idea in this topic is about knowledge. A pointer's declared type tells you what the pointer can see. A pointer to a base class gives you access to the base class's view of the object, and nothing more. The derived class may add members of its own, but the base-class side of the code has no information about them.

Derived objectone object, two viewsBase membersvisible through BasepointerBase pointersees the base view onlyDerived membersnot visible through Basepointercontainscontainsreaches
When a Base pointer refers to a Derived object, which parts of the object can the pointer reach, and which derived members are hidden from it?

Read this diagram as a statement about visibility. The object really does have derived members, and any copy of the whole object must account for them. But the code that holds the base pointer can only reach the base members. The default operation has no basis for copying the derived part correctly, because it has no way to reach that part.

What do you think happens?

A Derived object has a base part and a derived part. Through a Base pointer, can the code reach the derived part?

  • Yes, because the object really contains the derived members

  • No, the base-class view has no information about derived members

Reveal answer
Answer:

No, the base-class view has no information about derived members

The declared type of the pointer decides what the pointer can see. The object contains the derived members, but a Base pointer gives access only to the base class's view of the object.

Why a Base Class Cannot Copy What It Cannot See

Definition

A default copy or move operation is generated for the class in which it appears. A default operation for a base class therefore reflects only the base class's members. It has no way to reach members that exist only in a derived class.

This is the core reason the defaults cannot be assumed correct in a hierarchy. When a copy or move must duplicate the whole object, the operation needs to know every member it has to duplicate. Through only a base pointer, that knowledge is missing. The base class is not careless; it simply has no information about the members it would need to handle.

cpp
Notes on the code
  • Derived adds a member, extra, that does not exist in Base
  • The Base pointer p is declared as Base*, so it sees only the Base view of d
  • Copying through *p uses the Base side of the object, which has no knowledge of extra

Watching a Default Copy Lose Derived Data

Now trace what happens when a default copy or move runs on a derived object through its base-class part. Start from the source object and follow what the destination receives. The diagram below compares the state before and after the operation.

Source Base membersid = 7Destination Basemembersid = 7Source derivedmembersextra = 5Destination derivedmembersnot reachedcopiedno basis to copy
When a default copy runs through the base-class part, what happens to the derived members in the source and in the destination?
Worked example

Tracing a Base-Level Copy

A Derived object has id set to 7 in its base part and extra set to 5 in its derived part. A default copy runs through a Base pointer to that object. What does the destination receive?

  1. 1
    Identify the view

    The copy goes through a Base pointer, so the operation sees the Base members only.

  2. 2
    Check what the base operation knows

    The default operation for Base was generated for Base, so it reflects only Base members such as id.

  3. 3
    Consider the derived member

    The member extra exists only in Derived. The Base-level operation has no way to reach it, so it has no basis for copying it.

Result

The destination receives the base member id = 7. The derived member extra is not reached by the base-level copy, which is the silent loss the hierarchy warns about.

Deciding When to Delete Copy and Move

Given the problem above, the source gives a direct recommendation: the best thing to do is usually to delete the default copy and move operations. Deleting them means eliminating the default definitions of those two operations for the class. The hierarchy then no longer offers a default copy or move that would silently do the wrong thing.

Class in ahierarchy?base or derivedDefault copy and movecorrect?can the base reach allmembersKeep defaultsself-contained classDelete copy and moveusual remedyyesnoyesno
Given a class in a hierarchy, what decision path leads to deleting the copy and move operations instead of keeping the defaults?

Notice that the recommendation names both operations. Copy and move are handled together because both face the same limitation: each needs to know the derived members in order to duplicate or transfer them. Deleting only one of them would leave the other path open to the same silent problem.

cpp
Notes on the code
  • The copy constructor is deleted, removing its default definition
  • The copy assignment operator is deleted
  • The move constructor and move assignment operator are deleted as well, because copy and move are handled together

A deleted operation is removed from the class, so code that tries to use it does not get a silent default. In practice, the caller of a deleted copy or move is stopped at the point of use rather than receiving a copy that loses data. A defaulted operation, by contrast, still exists and runs the generated code. The source's guidance is that each base class should be checked on its own terms before deciding between these two.

Mistakes When Reasoning About Hierarchies

  • Common mistake

    Assuming the default copy works because the class has no obvious problems

    What it looks like

    A base class holds one int and looks simple, so its default copy seems fine.

    Why it fails

    The default is generated from the base class's own members. It cannot know about members that only a derived class adds.

    Do this instead

    Check whether the class is part of a hierarchy and whether derived members exist before trusting the default.

  • Common mistake

    Believing a base pointer gives access to everything the object contains

    What it looks like

    A Base pointer to a Derived object is used to read a derived member.

    Why it fails

    The declared type of the pointer decides what it can see. A Base pointer reaches the base view only.

    Do this instead

    Use a pointer or reference of the derived type when derived members must be accessed, or redesign the operation.

  • Common mistake

    Deleting only the copy operations and forgetting the move operations

    Why it fails

    Copy and move face the same limitation, because each needs to know the derived members.

    Do this instead

    Delete the copy and move operations together, as the source recommends.

  • Common mistake

    Treating a silent wrong result as a loud failure

    Why it fails

    The source describes the default as quietly doing the wrong thing, so no error may appear during testing.

    Do this instead

    Reason about the design in advance rather than relying on the program to report the problem.

Checking Your Reasoning on a New Class

MEDIUM

A class Shape is the base of Circle and Square. Shape has one member, name. Circle adds a radius member and Square adds a side member. Decide whether Shape should keep its default copy and move operations or delete them, and explain your reasoning in terms of what a Shape-level operation can and cannot reach.

Hints
  • Ask which members a Shape-level copy can see, and whether radius and side are among them.
  • Consider the source's point that each base class should be checked on its own terms.
  • Compare your answer with the flowchart: is the default correct for a class whose derived members are invisible to it?

Key Takeaways

  1. 1
    Default copy and move operations are generated for a class and reflect only that class's members, so they can fail in a class hierarchy.
  2. 2
    A base-class pointer reveals the base view of a derived object and does not expose the derived members.
  3. 3
    Because copy and move both need to know the derived members, the usual remedy is to delete both operations.
  4. 4
    Each base class should be checked on its own terms before deciding whether to delete or keep the defaults.

At a glance

  • Default copy and move operations are generated for the class in which they appear, so a base class's defaults reflect only its own members.
  • A base-class pointer reveals the base view of a derived object and hides the derived members from the operation.
  • The usual remedy is to delete the default copy and move operations together, since both need knowledge of the derived members.
  • Each base class should be checked on its own terms before deciding between deleted and defaulted operations.

Related concepts

More concepts starting with S