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.
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.
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?
Reveal 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
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.
- 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.
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?
- 1Identify the view
The copy goes through a Base pointer, so the operation sees the Base members only.
- 2Check what the base operation knows
The default operation for Base was generated for Base, so it reflects only Base members such as id.
- 3Consider 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.
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.
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.
- 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 likeA base class holds one int and looks simple, so its default copy seems fine.
Why it failsThe default is generated from the base class's own members. It cannot know about members that only a derived class adds.
Do this insteadCheck 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 likeA Base pointer to a Derived object is used to read a derived member.
Why it failsThe declared type of the pointer decides what it can see. A Base pointer reaches the base view only.
Do this insteadUse 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 failsCopy and move face the same limitation, because each needs to know the derived members.
Do this insteadDelete the copy and move operations together, as the source recommends.
Common mistake Treating a silent wrong result as a loud failure
Why it failsThe source describes the default as quietly doing the wrong thing, so no error may appear during testing.
Do this insteadReason about the design in advance rather than relying on the program to report the problem.
Checking Your Reasoning on a New Class
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
- 1Default copy and move operations are generated for a class and reflect only that class's members, so they can fail in a class hierarchy.
- 2A base-class pointer reveals the base view of a derived object and does not expose the derived members.
- 3Because copy and move both need to know the derived members, the usual remedy is to delete both operations.
- 4Each 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.