Header vs Source Files
Headers vs Source Files
Header files are the primary method for including external library code into your own project. As mentioned in the page on preprocessor directives, the #include directive copies the target file into your own source file, one to one.
The main objective is to provide an easy mechanism for making forward-declarations for external functions available to your own code. For example, it would be hard to know what the exact function signature for printf is between various macros potentially changing its definition, and it relying on lots of other compiler/library implementation details. So instead of having to type out a forward declaration, the stdio.h header can be included, that will contain the correct printf signature.
A simplified example of this looks as follows:
#include <stdio.h>
int main() {
int a = 5;
myfunction(a);
return 0;
}
void myfunction(int i) {
printf("%d", i);
}
This code will fail to compile. The compiler reads the source code from top to bottom, and when it arrives at line 5, myfunction has not yet been declared, so it will throw a compile error. To fix this, we would need to insert a forward declaration for myfunction before main:
void myfunction(int i);
This forward declaration notifies the compiler that somewhere in the code exists a function named myfunction that takes a single int parameter and returns void (the actual call to the function is then resolved during the linking process). Then, when the compiler reaches line 5, it is aware of the myfunction function, and so it can compile the code correctly.
In the case of something like printf, the forward declaration is in the stdio.h header, and the actual implementation of printf is somewhere among the pre-compiled C standard library that gets linked into the program.
So whats the difference from a source file?
In contrast to header files, source files contain the actual implementation for a function. One way you can think about it is that source files contain your code, and header files are a convenience feature for letting other source files know what functions you have, so they can use them too.
This makes the most sense with a concrete example. Take the code for LRI’s Rocket Control Interface. Under the src folder, you can see a collection of .cpp files containing the actual C++ implementation. Under the include folder, you can see a matching header file for every source file. These headers are how the functions contained in the C++ files are exposed to the rest of the program. This is not the only way to organize your code, but it is one way that clearly links function forward declarations to where their implementations are.
You may notice that in some cases, not everything from a source file is present in the header file. This is another way headers are useful; they allow you to selectively choose what you actually want to have available to the rest of the program. [1]
Header Guards
Consider a project with three files:
header1.h:
... some very useful stuff ...
header2.h:
// For some reason we need header1 here
#include "header1.h"
... some very useful stuff ...
main.c:
#include "header1.h"
#include "header2.h"
int main() {
... some very awesome code that uses
stuff from both header1 and header2...
return 0;
}
Here we have a piece of code that relies on the contents of both header1.h and header2.h. However, note that header2.h also includes header1.h. If you recall from the section on preprocessor directives, the #include directive directly copies and pastes the contents of one file at the location of the directive. The compiler will first hit line 1, where it will see that it needs to include the contents of header1 at that location.
However, the interesting behavior is when we go to include header2. The compiler will paste in the contents for header2, but it will see that header2 has its own includes, namely header1. So, it will for a second time copy in the contents of header1. While this won’t break anything since header1 just contains some forward declarations which can be repeated, it is wasteful and ends up double copying code. While this may not be an issue for small, lightweight headers, this becomes a problem if header1 includes a bunch of other headers, which may also include other headers, and so on and so forth until you have several thousand lines that get duplicated. Not to mention the headers included by header1 may also end up getting duplicated within each inclusion of header1, so you can end up with code repeated many times where it is completely unnecessary.
To counteract this, we use header guards to prevent a header file from being re-copied if it has already been included before. Header guards are actually very simple, and use some preprocessor directives we are already familiar with. Take these alternate versions of header1 and header2:
header1.h:
#ifndef HEADER1_H
#define HEADER1_H
... some very useful stuff ...
#endif
header2.h:
#ifndef HEADER2_H
#define HEADER2_H
... some very useful stuff ...
#endif
Now, the first time the compiler pastes header1 into main.c, it sees that the macro HEADER1_H has not already been defined, so it is free to include the rest of the contents. It also now defines HEADER1_H. The next time around when header1 is included via header2, the compiler sees that HEADER1_H has already been defined, and so the entire body of the ifndef at the top of header1 can be completely omitted, thus stopping any code from getting duplicated. Those 3 lines together are what are known as a header guard. [2] All header files in your project should utilize header guards.
Appropriate Uses of Headers
The most common mistake to make with header files is putting function definitions in a header, like so (include guards omitted for brevity):
header.h:
#include <stdio.h>
void myfunction(int i) {
printf("number: %d", i);
}
This is very bad!! Recall that the intention of header files is that they are included in multiple files to provide convenient forward declarations for functions and variables. If you write a full definition for a function in a header, then include this header in 2 different source files, you will get errors when compiling the program that there are 2 functions with the same name. This class of errors are known as linkage errors, since they occur during the linking process.
This happens for a very simple reason: remember that headers are simply copied into the sources files in which they are included. So in the situation above, you will end up with 2 source files which both contain a full implementation for myfunction. The compiler (technically its the linker) sees that there are two functions with the exact same name, and throws an error.
For an example of what not to do (sorry to the code writer you’re getting thrown under the bus a little (they have since fixed this!! just fyi)), you can take a look at some older versions of the Electric Angel code, specifically one of the header files. You can see the full definition of __MAINSETUP is right there. Fortunately for them, this particular header file was only included in one place, so they did not actually get compile errors when trying to build the project. However, had they included Procedures.hpp in another one of their source files, they would have.
Inline Functions
If you don’t know what an inline function is, feel free to ignore this section.
Inlining a function is a way of telling the compiler that instead of emitting a function call when it encounters one in the code, it should put the implementation for a function in its place. This is handy for small functions that are just a simple getter or settter. Being able to use the body of the function directly without the stack setup/teardown overhead can offer some speed bonuses.
However, some programmers are tempted to declare all their functions inline, that way they can put function definitions into header files. This is standards-compliant, but it is bad practice, and inlining so many functions can have negative impacts on binary size especially if the inlined function is large and used frequently.
Generally, you should avoid the inline keyword, since in most cases the compiler will inline things behind the scenes anyways if it determines it is worthwhile.
Constants
One thing that is good to put in a header file are compile-time constants. These are constants that get replaced into the actual text of your program and not embedded into the runtime application, so there are no linkage errors to worry about!
One example of an acceptable constant to place in a header file is anything declared with #define, since all of these constants/macros get replaced at compile time. That means having #define MYVAR 5 in two places won’t create linkage errors, since macros are local to the translation unit.
C++ introduces another class of compile-time constants that are acceptable in headers: constexpr variables. Click the link to learn more.
One type of constant that you cannot place in a header is anything declared as regular const. This is because const variables are still present at runtime, and are still suceptible to linkage errors.
Some Exceptions
Like all things C/C++, there are a few exceptions to the above rule of no-definitions-in-headers, but these you should still not rely on.
The first, as mentioned above, is that inlined functions can be in headers without causing issues. Generally speaking though, it is best to avoid inlining.
The next exception is templates. Unlike regular functions, the actual definition for a template (also called a specialization, because its when you fill in the template parameters) is generated and tracked by the compiler, so it will ensure there is only one definition for you.
Finally, the last major exception is class members. For whatever reason, class member functions defined in the class definition itself (i.e. within the class {} section) are also OK to include in the header.
However, even with these exceptions, it is still best to avoid relying on them. For simplicity and readability sake, put your definitions in source files, and don’t even bother worrying about whether putting a definition in a header will cause problems.
Issues with header files
Unfortunately, header files are not a perfect system. Headers were introduces back in the 80s with the initial version of C, and have not been able to be updated in order to maintain backwards compatability, so the header system is quite dated. There have been a few attempts to introduce a more modern imports system like in Java or Python (namely C++20 modules), but support for them has been overwhelmingly lackluster.
The main problem with headers is that you have to keep track of definitions in multiple places, and if a single header file changes you now have to recompile all sources that depend on it, even if they dont depend on the exact change within.
- Technically, you could write your own forward declarations for things not present in a header file. However, there are more strict ways to preventing someone from accessing stuff inside one source file from another; namely anonymous namespaces and the static keyword. ↩︎
- You may also see some people compact their header guard to a single line:
#pragma once. This is very nice and convenient, however it is not actually standard C/C++ and appears nowhere in the standards for the languages. As such, it is technically a compiler-specific feature, and not something that needs to be universally supported (although you will find this feature in all major compilers including MSVC, GCC, and clang). As such, we in LRI will not use this syntax, since we aim to produce standards-compliant code. ↩︎
