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.
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?
Reveal 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.
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.
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.
- 1Edit one: change behind the boundary
The header
Vector.his untouched.user.cppdepends only on the interface, which still says the same thing, souser.cppis not affected. The change lives inside the implementation, soVector.cppis the file that must be handled again. - 2Edit two: change to the shared boundary
Vector.his now different. Bothuser.cppandVector.cppdepend on that header, so both are affected and both must be handled again. - 3Compare 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.
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 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
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.
| Question | Interface | Implementation |
|---|---|---|
| Who needs it? | Code that uses the part | Code that builds the part |
| What does it answer? | What to pass in and what comes back | How the result is actually computed |
| Can it change freely? | Only with care, since callers depend on it | Yes, as long as the interface stays the same |
| Where does it live in C++? | A declaration, typically in a header | The 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.
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.
- 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.
- 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 likeLooking at double sqrt(double); and expecting to find the algorithm in that line.
Why it failsThe declaration deliberately leaves out the body. It gives the return type, the name, and the parameter type, but not the computation itself.
Do this insteadUse 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 likeWriting code that reads the internal storage of a Vector directly, because the layout appears in the header.
Why it failsPrivate members belong to the implementation side. Code that depends on them is tied to details that can be revised.
Do this insteadCall 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 likeChanging a parameter type in the shared header and rebuilding only the implementation file.
Why it failsEvery 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 failsThe 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 insteadDesign 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.
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.
- 1Identify the call
Write down the function or member being called, the values passed in, and the value that came back.
- 2Find the declaration in the shared header
Because
user.cppdepends on the shared interface, the declaration the call must match lives inVector.h, not inuser.cppitself. - 3Compare 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.
- 4Decide 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 insideVector.cpp.
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
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.
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
- 1Modularity divides a program into independent pieces, each with a clear purpose and a clear boundary.
- 2Two source files can share an interface through a shared header and remain separately compilable, as long as they are otherwise independent of each other.
- 3A change behind the boundary stays local, while a change to the shared interface affects every file that depends on it.
- 4Exploiting modularity follows three steps in order: logical structure, then language features, then physical files.
- 5An 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.