C++ has a compilation model where each translation unit is compiled independently. A translation unit is usually each .cc or .cpp source file plus all the headers it includes. Because translation units can be compiled independently, they can also be compiled in parallel or incrementally: after any change I only need to recompile changed translation units. This speeds up compilation of large projects a lot. But note that if a header is changed, all translation units that include the header have to be recompiled.
How can one source file call functions from another file if those files are part of different translation units? The other file might not have been compiled yet! The solution is that the header includes any information necessary so that the compiler can handle a call to that function, without needing to know the implementation of the function. This function declaration includes the function name, the return type, and the argument types. In contrast, a function definition contains the implementation of a function.
The compiler creates object code for each translation unit. Then a linker combines the object code into an executable program. The linker sees when there are function calls between translation units and fixes the addresses in the object code so that the calls go to the correct function. If we called a function that was declared but not defined, the linker shows an error.
There is another kind of error: we might have provided a function definition for the same function in multiple translation units! In C++, this is forbidden due to the One Definition Rule (ODR), which applies to functions and also to global variables. Because each function must be defined once, we usually put the function declaration in a header and put the definition into a corresponding source file. If we had put the complete function definition into the header, it would have been compiled into multiple translation units – an ODR-violation.
Sometimes, it is useful or even necessary to put functions into a header. For example, C++ templates must be fully defined in every translation unit where they are used so that they can be specialized to the template arguments. C++ offers two ways to deal with function in headers:
The ODR only applies to functions that are visible outside of this translation unit (have external linkage). If we give the function internal linkage it becomes private to the translation unit. If other translation units have a function of the same name (for example, by including the same header that defines the function), it's completely separate as far as C++ is concerned. We can request internal linkage by declaring a free function
static (note that this is different from member functions within a class definition!) or by putting it into an anonymous namespace.
Alternatively, we can disable the ODR by defining a function
inline. An inline function must be defined in the same translation unit where it was called (possibly in a header). So it doesn't have to link to a different translation unit. If the linker sees definitions for the same inline function in multiple translation units, it doesn't raise an error: by defining a function as inline we promise that all these definitions are identical, so the linker can discard any duplicate definitions.
Unfortunately, headers are not perfect. They have to include far more than only the function declarations we want to make publicly visible. In future C++ versions, modules will allow us to explicitly export specific declarations from a header.
Why don't other languages have to deal with headers?
- C++ inherited headers from the C language where headers were a kind of performance hack: by dividing the program into smaller compilation units with headers, we can compile a larger program than would fit into memory.
- C++ mostly lost these performance advantages because C++ programs have much more stuff in their headers, importantly templates from the standard library. These templates like
std::vector have to be recompiled in each translation unit.
- Rust is a modern C++-like language without headers. It has much larger translation units (roughly, one translation unit per library). When a translation unit is compiled, it generates the necessary metadata to allow other code to call into that library. But this also means that there cannot be circular dependencies between translation units (which C++'s headers allow).
Are headers a kind of abstraction? Usually yes, but not necessarily. They allow us to separate the public interface (like a function declaration) from internal details (like a function definition). Unfortunately, C++ often makes it necessary to put “private” information into the header (such as
private class members). This is because the header does not only describe an Application Programming Interface (API) but also an Application Binary Interface (ABI) – not just which calls are allowed but exactly how those calls will be performed, down to the memory layout of all function arguments. To abstract over such implementation details as well, we would have to apply object-oriented techniques ourselves.