Concepts / Abstract Classes and Pure Virtual Functions

Abstract Classes and Pure Virtual Functions

An abstract class interface defines the operations that callers may rely on, and code written against it does not need to know which implementation supplies them.

  • Programming

A Function That Names No Class

Most code you write eventually has to accept data whose exact kind is decided somewhere else. A function may receive a container, a shape, or a stream without knowing which concrete class was used to build it. An abstract class interface is the tool that makes this possible. It defines the operations that callers may rely on, and code written against it does not need to know which implementation supplies them.

This article follows one small function written in that style. The function is called use(), and its parameter is a Container. The goal is to read it the way a careful debugger would: first by asking what the function is allowed to depend on, and only then by asking what happens when it runs. Keep that order in mind throughout, because it is the fastest way to find a fault in interface-based code.

What do you think happens?

Before you trace anything: which names inside use() must exist for it to work, and where do those names come from?

  • The names of the concrete classes that might be passed in

  • Only the two operations that the Container interface provides

  • Every member of every class in the program

Reveal answer
Answer:

Only the two operations that the Container interface provides: a size query and indexed access.

The function body is phrased entirely in terms of what a Container can do. It never mentions a concrete class by name, so the only names it depends on come through the interface. Hold on to your answer, because a mismatch between expected and actual output will point you to these same names.

Tracing the Two Operations use() Needs

Look at the parameter type first. The function asks for a Container, not for a particular class with a particular name. Everything the body does is phrased in terms of what a Container can do: report how many elements it holds, and return the element at a given position. Nothing in the body mentions how those elements are stored, which class is handling the request, or what memory layout sits underneath.

The function depends on exactly two operations of its parameter. The first is a size query, used once to fix the loop bound. The second is indexed access, used once per loop iteration to fetch and print an element. The loop itself is ordinary counting code. Its only link to the outside world is through those two operations.

Container parameterthe only inputsize querycalled onceloop boundfixed by the sizeindexed accesscalled once per iterationprinted lineone per elementasked for countsetsdrives loopfetches element
Which operations does use() call, in what order, and where does each result go? The size query runs once and sets the loop bound; the indexed access runs once per iteration and feeds one printed line.
Worked example

Following use() through a three-element container

Suppose a Container holds three elements and is passed to use(). Trace which interface operations run and what the function prints. This scenario is invented for illustration; the source describes the structure but gives no concrete data.

  1. 1
    Size query

    use() asks the Container how many elements it holds. The answer is 3, and that value fixes the loop bound.

  2. 2
    Iteration one

    The loop index starts at 0. use() performs indexed access at position 0 and prints the element it receives.

  3. 3
    Iteration two

    The index moves to 1. Indexed access at position 1 runs, and its result is printed.

  4. 4
    Iteration three

    The index moves to 2. Indexed access at position 2 runs, and the third line is printed. The index then reaches the bound and the loop ends.

Result

Exactly one size query and three indexed accesses run, producing three printed lines. Notice that use() performed the same steps regardless of how the Container stored its elements.

The Interface as the Stable Middle

This narrow dependence is the whole point. If the function needs only these two operations, then any type that provides them can be handed to it. The function does not need to know which of several possible classes it is working with, because the names it uses are resolved through the interface rather than through a specific implementation.

use() functiondepends only on interfaceConcrete class Ageneric placeholderContainer interfacesize query and indexedaccessConcrete class Bgeneric placeholderConcrete class Cgeneric placeholderdepends only onprovides operationsprovides operationsprovides operations
What does the calling function connect to directly, and what sits behind the interface? The function connects only to the Container interface; the concrete classes each provide the operations that interface names, and any of them can be swapped in.

The diagram shows the three parts of the design. The function sits on one side and depends only on the interface. The interface is the stable point in the middle. The concrete classes sit on the other side, and each provides the operations the interface names. The labels for the concrete classes are generic placeholders, since the source describes the idea of a variety of classes without naming any particular ones.

Definition

A polymorphic type is a class that provides an interface to a variety of other classes. Its value lies in the operations it promises: code that uses those operations works with any class that supplies them.

This is why use() works with any class supplying a size query and indexed access. The function was written against the interface, so the interface is what makes it reusable. An abstract class is the place where such an interface is declared, and pure virtual functions are the operations that concrete subclasses must supply. The source does not give the declaration syntax, so the important idea to take from this section is the division of responsibility: the interface states what callers may rely on, and the implementation decides how those operations are carried out.

Dispatching a Call Through the Interface

When use() invokes an operation, the call is made through the interface, but something concrete has to answer it. The source puts this precisely: when behavior diverges from expectation, inspect the interface calls and the object that answers them, since the calling function itself names no concrete class. The calling code is never the place where the answer is decided.

use() callernames only the interfaceContainer interfacedeclares the operationsconcrete objectsupplies the answer1. size query2. request for count3. count returned4. indexed access5. request for element6. element returned
Which steps happen when use() invokes an interface operation, and which object produces the answer? The caller sends each request to the interface, and the concrete object behind it supplies the result.

Read the sequence as a conversation between three actors. The caller only ever talks about operations on the interface. The concrete object is the one whose behavior actually produces each returned value. A surprising result therefore has exactly two possible origins: the operation as use() requested it, or the implementation that answered it.

Where a Wrong Result Starts

Suppose the run does not match your prediction. The function's own text cannot introduce a difference by naming a different class, because it names none. So the search can be narrowed quickly. Use this ordering as a checklist, since the function body has only two interface touchpoints.

  1. 1
    Check the number of lines printed. If the count is wrong, the size query is the first place to look, because it set the loop bound.
  2. 2
    If the count is right but the content is wrong, the indexed access operation is the suspect, because it supplied each printed element.
  3. 3
    Once you know which operation is responsible, ask which object answered it. The concrete object behind the interface is where the actual value was produced.
  4. 4
    Only after these checks should you read further into the program. The function's body is short, so the faulty call is usually identified before a full read is needed.

Practice: Locate the Fault

MEDIUM

A function built the same way as use() prints four lines when the Container you pass in holds three elements. The printed values are otherwise correct. Using the checklist from the previous section, decide which operation to inspect first, and explain which object you would examine to find the cause. This scenario is invented for practice.

Hints
  • The number of lines printed depends on the loop bound, and the loop bound comes from the size query.
  • Because the content of each line is correct, the indexed access operation is probably not the first suspect.
  • Ask which object answers the size query, since the calling function names no concrete class.

Common Mistakes When Reading Interface Code

  • Common mistake

    Looking for the concrete class name inside the function

    What it looks like

    Searching the body of use() for the name of a specific class to find out what it is working with.

    Why it fails

    The function names no concrete class. Its parameter is a Container, and everything in the body is expressed through that interface.

    Do this instead

    Identify the operations the function calls, then find the object that answers them at run time.

  • Common mistake

    Assuming every operation of the interface is used

    What it looks like

    Trying to trace every member of the Container, including ones use() never calls.

    Why it fails

    The function depends on exactly two operations: a size query and indexed access. Other parts of the implementation are never touched by this function.

    Do this instead

    List only the operations the function actually calls. Those are the only ones that can affect its output.

  • Common mistake

    Debugging the loop first when the output is wrong

    Why it fails

    The loop is ordinary counting code. Its only connection to the outside world runs through the two interface operations, so a wrong value usually enters there.

  • Common mistake

    Treating a polymorphic type as one specific class

    What it looks like

    Believing that a function accepting a Container only works with one particular Container class.

    Why it fails

    A polymorphic type is defined by the interface it provides. Any class supplying the required operations can be handed to the function.

    Do this instead

    Describe a polymorphic type by its interface, not by a single implementation.

Key Takeaways

  • An abstract class interface defines the operations callers may rely on, so code written against it does not need to know which implementation supplies them.
  • The function use() depends on exactly two operations of its Container parameter: a size query used to fix the loop bound, and indexed access used once per iteration.
  • When an interface-based function misbehaves, inspect the interface calls and the object that answers them, because the calling function names no concrete class.
  • A polymorphic type is a class that provides an interface to a variety of other classes, and any class supplying the required operations can be used in its place.

Related concepts

More concepts starting with A