Concepts / Interfaces and Implementations

Interfaces and Implementations

Keeping the link limited to the shared interface means each file can be handled by itself, and the work of building the program can be divided accordingly.

  • Programming

Why Large Programs Need Boundaries

Imagine a program that started as a single source file of a few hundred lines. Over time it grows until that one long file is hard to read, hard to change, and slow to rebuild after every small edit. A common response is to split the work into several pieces, each responsible for one part of the job. Each piece can then be read, reasoned about, and changed with less worry about the others. This is the basic idea behind modularity: a program is organized as a collection of modules that each have a clear purpose and a clear boundary.

What do you think happens?

Suppose you improve how the Vector class stores its numbers internally, but you leave its public interface in Vector.h exactly as it was. Which source files must be handled again?

  • Only the file that implements Vector

  • Only the file that uses Vector

  • Every source file in the program

  • None of the files, because the interface did not change

Reveal answer
Answer:

Only the file that implements Vector

A change behind the boundary stays local. The implementation file is the one that carries out the Vector work, so it is the file that has to be handled again. The file that uses Vector depends only on the shared interface, which has not changed.

Splitting code into pieces is only half of the story. The benefit becomes real when those pieces can also be compiled on their own. That practice is called separate compilation, and it is what this article focuses on. The key question is how two pieces can depend on each other in a controlled way, so that one can be built without rebuilding the other.

Tracing a Change Through Three Files

Consider three files. Vector.h holds the information that describes the Vector interface: what a Vector offers and how other code may call it. The file user.cpp contains code that makes use of a Vector. The file Vector.cpp contains the code that carries out the Vector work. Both user.cpp and Vector.cpp rely on the same interface information, and that shared information lives in Vector.h.

Vector.hinterfaceuser.cppuses VectorVector.cppcarries out Vector workdepends on interfacedepends on interface
Which source files must be recompiled when the shared header changes, and which can be left alone? Both source files point toward Vector.h, and neither points toward the other.

The diagram shows the key structure. Both source files point toward Vector.h, and neither points toward the other. In other words, user.cpp and Vector.cpp share the interface but are otherwise independent of one another. The shared header is the only thing that ties them together, and that is exactly what makes it possible to treat them as separate units of compilation.

The word independent carries the weight of that claim. If user.cpp and Vector.cpp were tangled together, with each reaching into the internal details of the other, then neither could be handled without the other. Keeping the link limited to the shared interface means each file can be handled by itself, and the work of building the program can be divided accordingly.

Worked example

Following two different changes

The developer makes two separate edits to the Vector code. In the first, the internal workings of Vector change but the interface in Vector.h stays the same. In the second, the interface in Vector.h changes, for example by altering what a Vector function accepts. For each edit, decide which files are affected.

  1. 1
    Edit one: change behind the boundary

    The header Vector.h is untouched. user.cpp depends only on the interface, which still says the same thing, so user.cpp is not affected. The change lives inside the implementation, so Vector.cpp is the file that must be handled again.

  2. 2
    Edit two: change to the shared boundary

    Vector.h is now different. Both user.cpp and Vector.cpp depend on that header, so both are affected and both must be handled again.

  3. 3
    Compare the two results

    A change to the shared boundary ripples outward to every file that depends on it. A change behind the boundary stays local to one file. Tracing the effect of a change is useful precisely because separate compilation lets you see this difference without rebuilding everything by habit.

Result

Edit one affects only Vector.cpp. Edit two affects both user.cpp and Vector.cpp, because both rely on the shared header.

Three Steps Toward Modularity

The source describes a recommended approach with three parts, and the order matters. First, the designer should maximize modularity, meaning the program should be divided into pieces that are as independent as sensible. Second, that modularity should be represented logically through the features the language provides, so the boundaries exist in the design and are checked by the language. Third, the modularity should be exploited physically through files, so that each logical piece lives in a file that can be compiled separately.

Logical structurewhere the boundaries belongLanguage featuresboundaries the compiler cancheckPhysical filespieces that compileseparatelyboundaries are expressedpieces map onto files
In what order are the logical structure, the language features, and the physical files decided, and how does each step feed the next?

Logical structure comes first because it determines where the boundaries belong. Language features then express those boundaries in a form the compiler can check. Only after that does it make sense to map the pieces onto files, which is the step that makes separate compilation possible in practice. Reversing the order tends to produce files that were split by convenience rather than by purpose, which is exactly the situation the approach is meant to avoid.

Interface Versus Implementation

Definition

Every part of a program has two faces. The interface is what another piece of code must know in order to use that part. The implementation is the actual work the part does to deliver its result.

A caller of a square root routine needs to know what to pass in and what comes back. It does not need to be told every step taken to compute the answer. Thinking of the two as different things lets you reason about one without reopening the other. Code that uses a part depends only on its interface, and the interface is the agreed boundary. The implementation sits behind that boundary and can change as long as the interface stays the same.

QuestionInterfaceImplementation
Who needs it?Code that uses the partCode that builds the part
What does it answer?What to pass in and what comes backHow the result is actually computed
Can it change freely?Only with care, since callers depend on itYes, as long as the interface stays the same
Where does it live in C++?A declaration, typically in a headerThe body, typically in a source file

Interface versus implementation for a program part

Reading a Function Declaration

C++ has a direct way to write down an interface: the declaration. A declaration gives everything needed to use a function or a type, while leaving out the body that does the actual work. Consider the declaration of the square root function, written as double sqrt(double); Read this line in pieces, as the anatomy below shows.

doublereturn type: the kind ofvalue given backsqrtname of the functiondoubleparameter type: what thecaller must supply;ends the declaration
Which part of the declaration gives the return type, which gives the name, and which tells the caller what input to supply?

The first double is the return type, the kind of value the function gives back. The name sqrt identifies the function. The parenthesized double is the parameter type, the kind of value a caller must supply. The trailing semicolon ends the declaration. Nothing in this line says how the square root is computed, and that omission is the point: a caller can use sqrt correctly from this line alone.

cpp
Notes on the code
  • double before the name: the return type
  • sqrt: the name the caller writes
  • double inside the parentheses: the parameter type the caller must supply
  • semicolon: the declaration ends here, with no body

Declaring a Type Through Its Interface

Functions are not the only thing that gets a declaration. A user-defined type, such as a class, can also be described by one. C++ supports user-defined types, and classes are the main example. Once a type like Vector is declared, the rest of the program can treat it as a new kind of value, one that it can create, index, and ask for its size. The class declaration is therefore an interface for a type, in the same way a function declaration is an interface for a function.

cpp
Notes on the code
  • public section: the operations a user relies on
  • private section: members that belong to the implementation side
  • elem and sz: how the numbers are arranged internally, hidden from callers

A program that uses Vector only through the declaration depends on the operations it names, such as creating a Vector, indexing it, and asking for its size. The way elem and sz are arranged internally is part of the implementation, and it is the piece that can be revised while callers keep working. Reading the public part of a class declaration is therefore reading the agreed boundary, and reading the private part tells you what the boundary is protecting.

Mistakes When Reading Declarations

  • Common mistake

    Assuming a declaration shows how a function computes its result

    What it looks like

    Looking at double sqrt(double); and expecting to find the algorithm in that line.

    Why it fails

    The declaration deliberately leaves out the body. It gives the return type, the name, and the parameter type, but not the computation itself.

    Do this instead

    Use the declaration to learn how to call the function. Look in the implementation file only when you need to know how the work is done.

  • Common mistake

    Treating the private members of a class as part of its interface

    What it looks like

    Writing code that reads the internal storage of a Vector directly, because the layout appears in the header.

    Why it fails

    Private members belong to the implementation side. Code that depends on them is tied to details that can be revised.

    Do this instead

    Call only the public operations the declaration names, so that the internal arrangement can change without breaking your code.

  • Common mistake

    Believing a change to any part of a header affects only the file that was edited

    What it looks like

    Changing a parameter type in the shared header and rebuilding only the implementation file.

    Why it fails

    Every source file that depends on the header is affected by a change to the shared boundary, so those files must be handled again.

  • Common mistake

    Assuming separate compilation is a switch that the language turns on by itself

    Why it fails

    The source is explicit that separate compilation depends on how the program is designed and how the implementation handles files. Splitting code into files does not by itself guarantee independent pieces.

    Do this instead

    Design the boundaries first, express them through language features, and only then map them onto files whose dependencies stay limited to the shared interface.

Tracing a Wrong Result to Its Declaration

Suppose a call in user.cpp returns a value that is not what you expected. Because the caller relies only on the interface, the first place to look is the declaration that the call must match. The declaration fixes the number and types of the inputs a caller supplies and the type of value that comes back. A mismatch between what the call supplies or expects and what the declaration states is a likely place to find the cause.

Worked example

Tracing a surprising result

A call to a Vector operation in user.cpp produces a result that does not match the expected value. Trace the wrong result back to the declaration the call must match.

  1. 1
    Identify the call

    Write down the function or member being called, the values passed in, and the value that came back.

  2. 2
    Find the declaration in the shared header

    Because user.cpp depends on the shared interface, the declaration the call must match lives in Vector.h, not in user.cpp itself.

  3. 3
    Compare inputs and output with the declaration

    Check the parameter types and the return type stated in the declaration against what the call actually supplies and what it actually receives. A difference here points toward the cause.

  4. 4
    Decide where the fix belongs

    If the declaration itself is what must change, that is a change to the shared boundary and affects every file that depends on Vector.h. If the declaration is correct and only the implementation misbehaves, the change stays inside Vector.cpp.

Result

Tracing from the wrong result back to the declaration narrows the search to the shared interface and shows whether the problem lies at the boundary or behind it.

Check Your Reading of a Header

EASY

A header declares the function double area(const Shape& s); A caller in another file writes double a = area(box); Using only the declaration, state three things the caller must know, and state which part of the declaration tells the caller that the function will not modify the shape.

Hints
  • The first double tells you the return type.
  • The parenthesized part tells you what the caller must supply.
  • The const keyword in the parameter describes what the function promises not to change.
MEDIUM

A teammate changes only the body of area in the implementation file so that it computes the result differently, but keeps the declaration identical. Which source files need to be handled again, and why? Then imagine instead that the teammate changes the parameter type in the declaration. Which files are affected now?

Hints
  • Ask whether the change lies behind the boundary or on the boundary.
  • Any file that depends on the shared header is affected by a change to the header.

Summary of the Interface Idea

  1. 1
    Modularity divides a program into independent pieces, each with a clear purpose and a clear boundary.
  2. 2
    Two source files can share an interface through a shared header and remain separately compilable, as long as they are otherwise independent of each other.
  3. 3
    A change behind the boundary stays local, while a change to the shared interface affects every file that depends on it.
  4. 4
    Exploiting modularity follows three steps in order: logical structure, then language features, then physical files.
  5. 5
    An interface tells a user what to call and what comes back, while the implementation holds the work; a C++ declaration expresses the interface without the body.

At a glance

  • Modularity divides a program into independent pieces, each with a clear purpose and a clear boundary.
  • Source files that share only an interface in a header can be compiled separately, and changes behind the boundary stay local.
  • The three-step approach runs in order: logical structure, language features, then physical files.
  • A C++ declaration tells a user the return type, the name, and the inputs to supply, without revealing the implementation.

Related concepts

More concepts starting with I