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.
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?
Reveal 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.
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.
- 1Size query
use()asks the Container how many elements it holds. The answer is 3, and that value fixes the loop bound. - 2Iteration one
The loop index starts at 0.
use()performs indexed access at position 0 and prints the element it receives. - 3Iteration two
The index moves to 1. Indexed access at position 1 runs, and its result is printed.
- 4Iteration 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.
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.
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.
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.
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.
- 1Check 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.
- 2If the count is right but the content is wrong, the indexed access operation is the suspect, because it supplied each printed element.
- 3Once you know which operation is responsible, ask which object answered it. The concrete object behind the interface is where the actual value was produced.
- 4Only 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
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 likeSearching the body of use() for the name of a specific class to find out what it is working with.
Why it failsThe function names no concrete class. Its parameter is a Container, and everything in the body is expressed through that interface.
Do this insteadIdentify 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 likeTrying to trace every member of the Container, including ones use() never calls.
Why it failsThe 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 insteadList 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 failsThe 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 likeBelieving that a function accepting a Container only works with one particular Container class.
Why it failsA polymorphic type is defined by the interface it provides. Any class supplying the required operations can be handed to the function.
Do this insteadDescribe 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.