Concepts / Abstract Classes and Pure Virtual Members

Abstract Classes and Pure Virtual Members

A derived class overrides a base member when it supplies its own implementation of a member the base already defines. To identify the overrides a class provides, list the members it defines itself and check which of them also exist in the base.

  • Programming

Starting From a Question About a Call

Most people meet inheritance through definitions: a base class, a derived class, and a list of rules. This article takes the opposite route. You will start with a small, real class hierarchy, follow what the program does, and only then name the vocabulary that describes what you observed. The hierarchy under study has a base class called Container and a derived class called Vector_container.

Keep one question in mind while you read: at each point where the program asks a container for an element or for its size, which implementation actually answers? Almost everything in this article is built around locating that answer and explaining why it is correct. If a result ever surprises you, the trace is where your expectation and the program's behavior split apart.

Reading the Derivation Line

The first line of the class declaration carries the whole relationship. In the header, the name Vector_container is followed by a colon, the word public, and the name Container. The word public can be read as 'is derived from' or 'is a subtype of'. So the declaration tells you that Vector_container is derived from Container, and that Container is a base of Vector_container.

classdeclares a classVector_containerthe derived class name:introduces the basepublicread as 'is derived from'Containerthe base class namederived from
What each token in the header line of Vector_container contributes to the relationship with Container.

Read left to right, the declaration names the derived class first, then the base after the colon. The word between them sets the access level of the inheritance. Here it is public, which the source reads as 'is derived from'. Your job when reading any derived class header is to find the derived name, the base name, and that access word, in that order.

What Vector_container Inherits and Replaces

The relationship is easiest to hold in mind as a small hierarchy. The base class sits at the top, the derived class sits beneath it, and the members that the derived class replaces are attached to the derived class. The diagram below shows the structure for this example. The source excerpt does not reproduce the declaration of Container, so the diagram shows only the members the source names for Vector_container.

Containerbase classVector_containerderived classoperator[]()overrides base membersize()overrides base memberdestructoroverrides base destructorvVector member, own database ofsuppliessuppliessuppliesholds
Vector_container sits beneath Container. It overrides the indexing operator, size, and the destructor, and it holds its own member v.

Inheritance gives a derived class the members of its base. A derived class can also supply its own implementation of a member that the base already defines. This is called overriding. The source names two members of Vector_container that do this with respect to Container: operator[]() and size(). Each replaces the corresponding base member with a version that works on the derived class's own data. In this example, the operator returns an element of the internal Vector v, and size returns the size of v.

Key point

A derived class overrides a base member when it supplies its own implementation of a member the base already defines. To identify the overrides a class provides, list the members it defines itself and check which of them also exist in the base.

Following a Call Through g and use

The reason inheritance matters becomes clear when you consider a function written against the base class. Suppose a function named use takes a reference to a Container. For use to work with a Container without knowing implementation details, some other function must create an object for use to operate on. The source gives this arrangement as a function g that creates a Vector_container and then passes it to use. The object is built as a derived class, but use only ever sees it through the Container interface.

gcreates the objectusetakes Container&Vector_containerobjectseen through Container1. creates2. passes object3. calls size()4. calls operator[]()
g creates a Vector_container and hands it to use. Each call that use makes on the container is answered by the implementation in Vector_container.
Worked example

Tracing which implementation answers

Using the structure described in the source, trace a call in which g passes a Vector_container to use, and use asks the container for its size. The example below is an illustrative trace written for this article, not a listing from the source.

  1. 1
    Step 1: g creates the object

    g builds a Vector_container. At this point the object is a Vector_container, with its own Vector member v.

  2. 2
    Step 2: g passes it to use

    use receives a Container reference. Inside use, the object is only known through the Container interface.

  3. 3
    Step 3: use asks for the size

    The request is for size. Ask which implementation answers: the one in the object's own class, which overrides the base member, is the one that works on v.

  4. 4
    Step 4: record the answering implementation

    Write down 'size answered by Vector_container::size, acting on v'. This is the entry you check against your expectation.

Result

The call to size reaches the Vector_container implementation, because that class overrides the base member, even though use only names Container.

The same procedure works when you trace a program yourself. Follow the call from g into use, then from use into whichever member it invokes, and at each step write down the implementation that answered. The first place where the implementation you wrote down disagrees with the one you expected is the place to investigate.

Why use Works Without Knowing the Implementation

The source makes the key point explicit. Because use does not know about Vector_container and only knows the Container interface, it works just as well for a different implementation of a Container. The function is not tied to one implementation, so a new class derived from Container can be given to use without changing use itself.

What do you think happens?

Inside use, the object is seen only as a Container. When use calls size() on it, which size() runs?

  • The size() declared in Container, because use is written against Container

  • The size() defined in Vector_container, because it overrides the base member

  • Both run, one after the other

  • Neither runs, because use cannot see the implementation

Reveal answer
Answer:

The size() defined in Vector_container runs.

Vector_container supplies its own size() that overrides the base member, and it returns the size of its internal Vector v. Because use relies only on the Container interface, it does not need to know which implementation answers. The override is the one that runs.

Key point

A function written against a base class interface, such as one that takes a Container reference, can operate on a derived class object because it relies only on that interface. The call is answered by the derived implementation, and the function's own code does not change.

Destructors and Member Cleanup

The destructor follows the same overriding pattern. The destructor of Vector_container overrides the destructor of Container. The source also points out a second, separate event: the member Vector v has its own destructor, and that member destructor is invoked implicitly when the destructor of Vector_container runs. The derived class does not need to write code to clean up its member, because the member's own destructor takes care of it.

Mistakes When Tracing Inheritance

  • Common mistake

    Assuming the base class version runs because the function parameter has the base type

    What it looks like

    Inside use, which takes a Container reference, a call to size() on a Vector_container object.

    Why it fails

    The parameter type controls which names are visible, not which implementation runs. The object's class determines the override that answers.

    Do this instead

    Ask which implementation answers, and check the object's class for an override of that member.

  • Common mistake

    Looking only at the derived class header and forgetting the members it inherits

    Why it fails

    The derivation line names the base, and the base members that are not overridden still come along with the derived class.

    Do this instead

    Read the derived class's own declarations, then identify which base members it overrides and which it simply inherits.

  • Common mistake

    Reading public as a decorative word

    Why it fails

    In the declaration, the word between the colon and the base name is part of the derivation, and it reads as 'is derived from'.

    Do this instead

    Include the access word when you identify the base and derived relationship.

  • Common mistake

    Assuming the member variable v is cleaned up by the derived destructor's code

    Why it fails

    The source says v's own destructor is invoked implicitly when the destructor of Vector_container runs.

    Do this instead

    Attribute cleanup of a member to that member's destructor, not to explicit code in the derived class.

Locating Where a Result Diverges

When an observed result differs from what you expected, do not start by rereading the whole program. Work through the trace in three questions. Which member answered the request? Which implementation of that member ran? Which object was supplied? Each answer narrows the search. If the member and the object are what you expected but the implementation is not, look for an override you missed. If the implementation is right but the object is not, trace where the object was created and what was passed along the way.

Practice Tracing the Container Calls

MEDIUM

A function g creates a Vector_container and passes it to use, which takes a Container reference. Use asks the object for its element at a given index. For this request, write down the member that answers, the implementation that runs, and the object it acts on. Then name the first place where your answer would differ if use had been given a different class derived from Container.

Hints
  • Start with the call from use, and name the member it invokes.
  • Ask whether Vector_container supplies its own version of that member.
  • Consider what changes if the object is a different derived class: which implementation would answer, and why would use still compile unchanged?

Key Takeaways

  • A derived class is declared with its base after a colon and an access word, as in Vector_container being derived from Container with public.
  • A derived class overrides a base member by supplying its own implementation, as Vector_container does for operator[](), size(), and the destructor.
  • A function written against a base interface, such as use(Container&), works on a derived object because it relies only on that interface, and the derived implementation answers each call.
  • To find where a result departs from expectation, trace which member answered, which implementation ran, and which object was supplied, at every step.

Related concepts

More concepts starting with A