Interfaces and Implementations of Classes
Separate compilation divides code into files that are compiled independently, so user code can depend on an interface rather than an implementation.
A File That Uses a Class It Did Not Write
Large programs are rarely written as one file. Separate compilation divides code into files that are compiled independently, and this division changes what a file has to know. A file that uses a class does not have to contain that class's implementation. It can depend on an interface instead. This article follows one user-side file from start to finish and asks, for every name it uses, which line brings that name into the file.
What do you think happens?
To be written and compiled, does user.cpp need the code that implements the Vector class, or does it need something smaller?
Reveal answer
It needs only the interface of Vector
The user file can be written, read, and compiled on its own. Its author depends on what the interface promises, not on the details inside the implementation.
Tracing the Three Sources of Names
The example file depends on three different sources of names: a file written by someone else (Vector.h), a standard library header (cmath), and the std namespace that the standard library uses. Before reading the code, look at what the function needs. The function sqrt_sum must know what a Vector is and how to ask it for its size and its elements, and it also calls sqrt. Each of those needs is supplied by one of the three include and using lines at the top of the file.
- The first line gives user code access to the Vector interface
- The second line brings in the declarations for standard math functions such as sqrt
- The third line makes standard library members visible without a prefix
- The loop condition i !=
v.size()uses the interface of Vector - The statement sum += sqrt(v[i]) adds the square root of each element to a running total
In the formula, $v_i$ stands for the element at index $i$ of the vector. The loop body computes the square root of each element and adds it to the running total, so the function returns the sum of those square roots. The function body combines three kinds of names: the size and indexing operations come from Vector.h, and sqrt comes from cmath. Each one traces back to a specific line at the top of the file.
Vector.h Supplies the Interface
The first line of user.cpp, #include "Vector.h", is the link between the user file and the class. The source comment states its purpose: it gets Vector's interface. The interface is the part of the class that other code is allowed to see and use. It tells the compiler what a Vector is and what operations exist on it, such as the size() call and the indexing operation v[i] used inside the loop. The user file does not need to see how those operations are carried out.
Why Separate Compilation Helps
Because the user file depends only on the interface, it can be written, read, and compiled on its own. The class's implementation lives in a separate file that is compiled independently. The diagram below follows the path from source files to a final executable. The user file and the implementation file each go through their own compilation step, and the class is connected to the user code at the later linking step, where the separately compiled parts are combined.
What the using namespace std Directive Controls
The line using namespace std; is a directive that makes the members of the std namespace visible in the file. Without this line, a standard library name such as sqrt would not be visible under its plain name. The same function can still be reached by writing the qualified name std::sqrt, which makes the namespace explicit at each use. The source points to a later section of the text for more detail on this directive.
- The include brings in the declaration of sqrt
- The qualified name std::sqrt works without any using directive
Mistakes When Tracing Names
Common mistake Assuming the using namespace std; line is what brings sqrt into scope
Why it failsThe cmath include supplies the declarations for sqrt. The using directive only controls whether the plain name can be written without the std prefix.
Do this insteadFind the include line for the declaration, then separately check whether the using directive lets you write the plain name.
Common mistake Looking for the body of Vector's operations in user.cpp
Why it failsThe user file depends on the interface. The way
size()and indexing are carried out lives in the implementation file.Do this insteadTrace names to Vector.h for what exists, and to Vector.cpp only when you need to know how an operation is carried out.
Common mistake Believing that the user file must be compiled together with the implementation before it can be checked
Practice Tracing Names in user.cpp
Using the user.cpp example, name the line that introduces the name Vector into the user file, the line that introduces the name sqrt, and the line that makes the plain name sqrt writable without a prefix. Then say whether the user file needs the implementation of Vector in order to compile.
Hints
- Look at the three lines at the top of the file, before the function begins
- The name Vector comes from the interface file, and the declaration of sqrt comes from the math header
- The using directive is the only one of the three lines that concerns the std prefix
Key Takeaways
- Separate compilation divides code into files that are compiled independently, so user code can depend on an interface rather than an implementation.
- The line #include "
Vector.h" gives user code access to Vector's interface, which lists what operations exist without showing how they are carried out. - The line #include <cmath> provides the declarations for standard math functions such as
sqrt(), while using namespace std; controls whether std members can be written without the std prefix. - To trace a name, find the include or using line that introduces it: Vector and its operations come from
Vector.h, sqrt from cmath, and the plain spelling of sqrt depends on the using directive.