92 / 163 · C++11 · 12 min
Polymorphic interfaces do not expose assignment: let leaves keep complete value semantics
Assignment through a base-class reference usually changes only the base subobject and does not represent complete polymorphic copying. Make assignment at the interface layer protected, let concrete leaves use normal copy, and use assignability checks to prove that clients cannot cross this boundary.
In this lesson
More Effective C++: 35 New Ways to Improve Your Programs and Designs
Have fully read all technical items Items 1–35 of the public 267-page Chinese composite PDF (PDF pages 11–240), the recommended reading and auto_ptr appendix (pages 240–247), and two additional articles (pages 247–267), and have directly verified all 31 embedded figures in the main text as well as the notation notes on page 9 by inspecting the figures. “full” refers only to all substantial chapters of this public Chinese text, not page-by-page verification of the English original or a commercial Chinese edition: the commercial translation’s edition is unverified; the first 4–5 pages of Chinese appear as boxes in both the source PDF and the images, were not recovered, and are not counted as technical main text. The publisher’s English Item 33 was also read for cross-checking; historical translator notes and code errors should not be treated as C++20 rules.
Edition, actual reading range, and original sources →First ask what copying means in the business
Item 33 discusses non-leaf class design from the angle of partial assignment: when two objects meet via base-class references, the statically visible assignment function can usually handle only base-class state. Even if the objects have the same dynamic type, one cannot assert from that that all derived members have been updated. Forcing assignment to be declared virtual then raises the question of whether different derived types can assign to each other.
In this example the rule interface is only responsible for answering an amount; it does not promise to replace an entire rule through the interface. A concrete fixed rule stores its own value; copying two fixed rules is a reasonable operation, but assignment through the abstract interface has no business meaning. Therefore excluding it from the interface is clearer than guessing the types at both ends at runtime.
Protected is not deleting all copying capability
The base class’s default constructor, copy constructor, and copy assignment are placed in the protected region. Thus derived classes can still generate normal copy operations, while external callers cannot directly perform base-class assignment. The base class keeps a public virtual destructor, allowing objects to be destroyed later through owning base-class pointers; the pure virtual query makes the interface itself uninstantiable.
The concrete leaf uses final to indicate that further inheritance is no longer promised here. It does not forbid copying, nor does it replace access control. The example first completes assignment on the leaf type, then observes the result via a const base-class reference, verifying that what was updated is the complete concrete value, rather than attempting to reproduce a dangerous partial update.
Express the prohibition with boundary checks, without running bad code
Two static_asserts respectively verify that the interface is not assignable and that the concrete type is assignable. is_assignable checks expression validity from a context unrelated to these two types, and therefore considers protected access. This is more reliable than a comment saying not to do this, and does not require mixing deliberately non-compiling statements into a runnable program.
This is not a requirement that all non-leaf classes be mechanically turned into abstract classes. If an existing model truly needs polymorphic copying, one should separately design an interface that explicitly returns a new object and state ownership and exception guarantees. The current need does not have this capability, so no clone framework is added. The value of the book’s advice is in making the design expose real semantics, not in adding layers.
Pitfalls
- Merely marking the derived class final will not prevent a caller from performing partial assignment through a still-publicly-assignable base-class interface.
- Directly = delete-ing base-class assignment will affect the assignment implicitly generated for derived classes; this example chooses protected default, preserving the leaf’s normal value semantics.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <type_traits>
class Rule {
protected:
Rule() = default;
Rule(const Rule&) = default;
Rule& operator=(const Rule&) = default;
public:
virtual ~Rule() = default;
virtual int amount() const noexcept = 0;
};
class FixedRule final : public Rule {
int amount_;
public:
explicit FixedRule(int amount) : amount_(amount) {}
int amount() const noexcept override { return amount_; }
};
static_assert(!std::is_assignable<Rule&, const Rule&>::value,
"The interface does not support assignment");
static_assert(std::is_copy_assignable<FixedRule>::value,
"Concrete values remain assignable");
int main() {
FixedRule first(7);
const FixedRule second(19);
first = second;
const Rule& view = first;
assert(view.amount() == 19);
assert(second.amount() == 19);
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-more-effective-cpp.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If Rule::operator= is changed to public default, which compile-time check will change?
Show a reference answer
is_assignable<Rule&, const Rule&>::value will become true, even though Rule remains an abstract class. Abstractness prevents creating standalone Rule objects, but does not prevent performing public assignment through a Rule reference pointing to an existing derived object. Therefore the first static_assert will fail, revealing that the interface boundary has been enlarged.
Check the sources
- More Effective C++ Item 33 — publisher full item
- C++ draft [class.copy.assign]
- C++ draft [meta.unary.prop] — is_assignable access checks
Drafts and official chapters change. The version mark is only the example’s minimum.