Skip to content

Module 8 — Misconceptions and Mistakes

Two different problems, fixed two different ways. A misconception is a wrong mental model, and only evidence removes it. A mistake is a concrete error, and information fixes it.

Every compiler message on this page was produced by compiling the snippet shown with g++ -std=c++17 -Wall -Wextra on the toolchain this course's examples are verified against.


Part 1 — Misconceptions

"An abstract class is a base class that forgot to implement something"

Why this feels true. = 0 looks like a placeholder, the way // TODO is. The name "pure virtual" suggests incompleteness rather than intent.

Where it breaks. It is not incomplete. It is a statement, enforced at compile time: anything claiming to be this must be able to do these things, and no instance of the general case exists.

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

int main() {
    Shape shape;                  // abstract
    Empty empty;                  // Empty : public Shape {} — still abstract
}
error: cannot declare variable 'shape' to be of abstract type 'Shape'
      |           ^~~~~
note:   because the following virtual functions are pure within 'Shape':
note:     'virtual double Shape::area() const'
error: cannot declare variable 'empty' to be of abstract type 'Empty'
note:   because the following virtual functions are pure within 'Empty':
note:     'virtual double Shape::area() const'

The correct model. The base has no answer on purpose. The note: lines are a checklist, and note that the second error names Shape::area again — because a derived class that does not override it inherits the obligation, and stays abstract.

The re-test. A class has five pure virtual functions and overrides four. Is it abstract? Where does the failure appear, and which line names the missing function?


"A definition makes a pure virtual function non-pure"

Why this feels true. For every other kind of function, having a body is having an implementation, and "pure" sounds like "bodyless".

Where it breaks. examples/module-08/05-pure-virtual-with-a-definition/main.cpp declares virtual std::string footer() const = 0; and defines it outside the class, and the base still cannot be instantiated:

And the base still cannot be instantiated:
    Exporter exporter;    error: cannot declare variable 'exporter' to be
                          of abstract type 'Exporter'
A definition does not make a pure virtual function non-pure. `= 0`
decides abstractness; the body only decides what a subclass can call.

The correct model. The declaration = 0 decides abstractness and the obligation to override. A definition, if you write one, is an implementation a subclass may reach by qualification — Exporter::footer() — and it is never called virtually, because there is no object of the abstract type to call it on. The two statements are independent, which is why = 0 in C++ means "you must supply this", not "no implementation exists".

The re-test. Why is a pure virtual destructor the one case where a definition is not optional? (§4 of Concepts.)


"An interface should offer everything a caller might need"

Why this feels true. A wider interface needs changing less often, and the whole benefit of an abstraction is that callers stop changing.

Where it breaks. Every function in an interface is a promise that every concrete class must keep. examples/module-08/02-interface-contracts/main.cpp keeps its interface at one function and the payoff is visible in its output — three channels, including a test double that sends nothing, and no line of the service knows which it has:

  sending through: recording
    nothing was sent. What the recorder captured:
      ayesha@example.edu | Fee due on Friday

A Printer interface with print(), flush(), scan(), staple() and email() forces a receipt printer to implement scan() — and the implementation will either throw, return false, or silently do nothing, and all three are lies the type system now permits.

The correct model. Wide interfaces do not reduce change; they relocate it, from callers to implementers — and implementers are more numerous. The test is one sentence: can every implementation honour every function without lying? The first one that cannot is the one that splits the interface. That principle has a name (interface segregation) and a module (Module 15).

The re-test. Printable has oneLine(). A class needs both a one-line summary and a full report. Should Printable gain fullReport()? Give the answer in terms of who pays.


"Abstract classes can be copied, as long as I do not instantiate them"

Why this feels true. Copying is a separate operation from constructing, and the class does have a copy constructor generated for it.

Where it breaks.

std::vector<Shape> shapes;      // will not compile: Shape is abstract
void print(const Shape shape);  // will not compile either

and if the base were concrete, the same code would compile and slice every element (Module 7, Example 4).

The correct model. Value semantics require a complete object, and an abstract type has no complete objects. Everything abstract is held by reference or pointer, which means every collection of them raises a lifetime question. That question is not an accident of C++ — it is the price of the abstraction, and it is why examples/module-08/01-shape-abstraction/main.cpp ends by handing that problem forward: a container of Shape* needs a delete loop, and a delete loop is exactly what Module 11 replaces with std::vector<std::unique_ptr<Shape>>.

The re-test. void f(const Printable& p) and void f(Printable p) — which compiles, and what does the other one tell you about the type?


"A derived class with a slightly wrong signature is fine as long as it compiles"

Why this feels true. With virtual functions that have implementations, a mismatched signature produces a new function that hides the base's — legal, silent, and Module 7's subject. The habit of expecting a diagnostic is not unreasonable.

Where it breaks. With a pure virtual function, the missing override has a second consequence: the derived class is still abstract. And the error appears where you use it, not where you wrote it:

error: invalid new-expression of abstract class type 'WrongConst'
   12 | class WrongConst : public Shape {
note:   because the following virtual functions are pure within 'WrongConst':
note:     'virtual double Shape::area() const'

with the hiding warning arriving separately:

warning: 'virtual double Shape::area() const' was hidden [-Woverloaded-virtual=]
note:   by 'double WrongConst::area()'

The correct model. Two messages, two files apart: the mismatch is silent at the declaration, and the abstractness is reported at the point of use. override is what joins them up — it makes the compiler report the mismatch where the mistake is. This is why Module 7's rule is not style advice, and it matters more here, because the alternative diagnostic points at the wrong line.

The re-test. Where would you look for the mistake if the only message you had was invalid new-expression of abstract class type?


"A pure virtual destructor is just a stylistic choice"

Why this feels true. It is a destructor; destructors are usually generated for you.

Where it breaks. A destructor's body always runs, so a pure virtual destructor must have a definition. Omit it and the failure is at link time, not compile time — and on the toolchain used here the message names nothing at all:

collect2.exe: error: ld returned 116 exit status

Elsewhere the same mistake reads undefined reference to 'Sink::~Sink()', which is at least searchable. Both mean the same thing: something declared is never defined.

The correct model. A destructor is not an extension point the way other virtual functions are: it runs, always, at the end of every complete object's life, up the whole chain. If you want an abstract base that no one can delete through a pointer, the tool is a protected non-virtual destructor, which makes delete p a compile error rather than a link error. The ordinary abstract base wants virtual ~Base() = default;.

The re-test. Name the two ways an abstract base's destructor can be declared, and what each one prevents.


"Every level of the hierarchy adds value, so depth is fine"

Why this feels true. Each level was added deliberately, for a reason. Retracing the reasons finds no step that was obviously wrong.

Where it breaks. Depth costs assumptions per level. The symptoms are behavioural, not structural: a level-four class that must override something in order to disable it; a level whose only contribution is convenience constructors; protected data that a leaf can corrupt. Section 8 of Concepts lists them, and examples/module-08/04-abstract-base-and-interface/main.cpp shows the alternative — a two-role class built from one stateful base and one interface, which is flat by construction.

The correct model. Depth is not measured in levels but in how far a change at the root reaches. An interface hierarchy adds one level and stops, which is why interfaces are the usual tool for extension. The rule of thumb: if "is-a" needs a qualification to be true, the hierarchy is one level too deep.

The re-test. A Square : public Rectangle is the classic qualification case. State the qualification, and say what it implies for the hierarchy.


Part 2 — Common programming mistakes

Symptom Cause Fix
error: cannot declare variable 'x' to be of abstract type 'X' Instantiating a class with at least one un-overridden pure virtual function Implement the functions the note: lines list; or hold the object by reference or pointer
The same error, naming a derived class The derived class did not override everything — most often a signature mismatch Read the note: list, then add override so the mismatch is reported at the declaration
error: invalid new-expression of abstract class type new on an abstract type The type is a contract, not an implementation; new the concrete class
error: conflicting return type specified for 'virtual float D::area() const' The override's return type differs from the base's Match the return type exactly — or use a covariant return, which only works for pointers and references to derived types
warning: 'virtual double Shape::area() const' was hidden A derived function whose signature differs hides the pure virtual, leaving the class abstract Fix the signature; keep override on it
A link error naming nothing, or undefined reference to 'X::~X()' A pure virtual destructor declared without a definition Define it: X::~X() = default;
Callers need dynamic_cast to do their job The interface is missing an operation, or is too general Add the operation to the contract, or split the interface
Every implementation throws on one function The interface is too wide Split it; see §6 of Concepts
A new kind of object means editing a switch in three files The dispatch was never moved behind the interface Module 7's refactor, applied to an interface rather than a base class

Mistake 1 — a derived class that is still abstract [L3 Modify]

Reproduce it.

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Empty : public Shape {};        // forgot to override area()

int main() {
    Empty empty;
}

What you see.

error: cannot declare variable 'empty' to be of abstract type 'Empty'
note:   because the following virtual functions are pure within 'Empty':
note:     'virtual double Shape::area() const'

Why. A derived class that does not override a pure virtual function inherits the obligation. The note: names the function — and note that it names the base's declaration, which is the quickest way to find what is missing even in a large hierarchy.

Fix it. Implement area(), with override:

class Empty : public Shape {
public:
    double area() const override { return 0; }
};

Prevent it. Add override at the moment you write the function, so a signature slip is reported at the declaration rather than at the point of use. When the error appears at a call site, read the note: list before you read anything else.


Mistake 2 — a const that went missing [L3 Modify]

Reproduce it.

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class WrongConst : public Shape {
public:
    double area() { return 3.14; }            // const missing: does not override
};

int main() {
    Shape* b = new WrongConst();
}

What you see.

warning: 'virtual double Shape::area() const' was hidden [-Woverloaded-virtual=]
note:   by 'double WrongConst::area()'
error: invalid new-expression of abstract class type 'WrongConst'
note:   because the following virtual functions are pure within 'WrongConst':
note:     'virtual double Shape::area() const'

Why. double area() and double area() const are different functions. The derived one hides the base's, and because the pure virtual is still un-overridden, WrongConst is still abstract. The warning appears at the derived declaration; the error appears where the object is created. This split is the single most confusing diagnostic in the module.

Fix it. Write double area() const override.

Prevent it. override. Write it on every function that is meant to override; it is checked at compile time, costs nothing, and collapses these two distant messages into one that points at your line.


Mistake 3 — the override's return type [L3 Modify]

Reproduce it.

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class WrongReturn : public Shape {
public:
    float area() const { return 3.14f; }      // float, not double
};

What you see.

error: conflicting return type specified for 'virtual float WrongReturn::area() const'
      |           ^~~~
note: overridden function is 'virtual double Shape::area() const'

Why. float and double are different types, so this is not an override — and because the return type conflicts rather than merely differing, the compiler reports it directly, unlike the const case. (A genuine exception exists: a covariant return type, where an override returns a pointer or reference to a class derived from the base's return type. It is worth knowing the rule exists and rare to need it.)

Fix it. Match the base exactly: double area() const override. If you want to know why the base chose double, that is a separate conversation — and a good one.

Prevent it. Derive the override from the base's declaration rather than from memory: copy the line, change the body, add override. When a base class is yours, consider virtual double area() const = 0; written once and pasted everywhere.


Mistake 4 — holding abstract objects by value [L3 Modify]

Reproduce it.

std::vector<Shape> shapes;        // abstract: will not compile
void print(const Shape shape);    // abstract: will not compile

and, when the base is concrete, the version that compiles and loses data:

std::vector<Person> people;
people.push_back(manager);        // compiles: slices the manager

What you see. For the abstract case, a compile error naming the abstract type; for the concrete case, nothing — output that describes base objects, with the derived data silently gone.

Why. Value semantics require a complete object, and an abstract type has no complete objects; a concrete base does, so the copy is legal and only the base part is copied.

Fix it. Hold handles:

std::vector<std::unique_ptr<Shape>> shapes;      // Module 11's ownership

Prevent it. Two greppable habits: never take a polymorphic parameter by value, and never put a polymorphic type in a container directly. Write the ownership decision down where the container is declared, because that is where it will be questioned.


Mistake 5 — a pure virtual destructor with no definition [L3 Modify]

Reproduce it.

class Sink {
public:
    virtual ~Sink() = 0;                       // pure, and never defined
    virtual void write(const std::string&) = 0;
};

class EventSink : public Sink {
public:
    ~EventSink() override = default;
    void write(const std::string&) override {}
};

int main() { EventSink sink; sink.write("x"); }

What you see. This compiles cleanly — no warnings, no errors — and then fails to link. On this toolchain the message is the uninformative:

collect2.exe: error: ld returned 116 exit status

Why. EventSink's destructor must call ~Sink(), and ~Sink() has no body. The compiler has nothing to complain about: the declaration is complete and legal. The linker is the one who notices, which is why this class of mistake feels different from every other one in the module.

Fix it. Define it, once, outside the class:

Sink::~Sink() = default;

Prevent it. Prefer virtual ~Base() = default; and make the class abstract through its other functions. Reach for a pure virtual destructor only when the intent is "this base must never be destroyed through a pointer to it" — and in that case prefer a protected non-virtual destructor, which fails at compile time with a message that names the line.


Next: Debugging — the contract audit, and a diagnosis that reads the note: lines before anything else.