Any C++ developer knows how to compile and link multiple compilation units together. The difficult part can be to determine which parts of the code should be separated in different compilation units. And how should the code be distributed between source and header files? Let’s start with a disclaimer: This is not the easiest topic, and there is no single solution. Source file organization can be done very differently in different projects. If you happen to work in a project where a style guide describes source file organization, stick to that.
If you don’t have such a style guide, here might be some thoughts that can help you create one. In the end one of the most important things in source file organization is consistency. It enables you and other developers in your team to find existing source files faster and to know where to put new ones.
Header source relationship
There are several things about the organization of header and source files that seem common sense. Yet there seem to be some more adventurous developers who like to question the status quo and mix things up. Don’t do this without a very good reason. The cost is that others who try to find their way through our sources may get confused.
One header per source file
The functions and classes we implement in our source files are not useful if they are not used in other parts of our program. To be able to do that, we need function declarations and class definitions of the implemented entities in those other locations. That’s what header files are used for.
The entities that are implemented in a given source file are best declared in a single corresponding header file. Both files should have the same file name prefix. That way, if we change or add something in a source file, there is a single canonical place where that change has to be reflected.
There are exceptions to that rule. The main function, DLL entry functions and the test cases in some test frameworks do not need to be declared elsewhere. If we put those entities in their own separate source files there will be no corresponding header at all.
At most one source file per header
If we have entities that are declared together in a header, that usually means they do belong together. They then also should be implemented together, which means in the same source file.
Doing so also reflects the single responsibility principle: The header file should not have two different source files that cause it to be changed. If on the other hand there are two entities that are not related enough to share an implementation file, they also ought to be declared in different header files.
Of course sometimes we have several different entities which form a component or subsystem and almost always are used together. Putting their implementation into a single source file does not seem right, but on the other hand we don’t want to have to include the same five headers everywhere. In such a situation, consider to write a subsystem header that includes the individual headers of the individual entities.
This header would of course not have any corresponding source file. Similarly, there are templates, abstract classes, inline functions and other stuff that is implemented right in the header and therefore does not need a source file. Having a header without an associated source file is perfectly OK.
Header source separation
Header only libraries are a common thing to have. We don’t have to compile and link them into our application. Yet this convenience comes at a cost: Things that are implemented in a header file have to be parsed and compiled into every translation unit that includes the header, unless it is part of a precompiled header. This can slow down compilation considerably.
Details in a header can have other drawbacks as well. Any dependency needed for implementation details has to be included in the header and therefore in the files that include it. Any minor change in the implementation of some inline function will trigger a recompilation of dependent code by the build system, even if that change does not affect the other code.
On the other hand, hiding every single implementation detail in the source file may prevent the optimizer from doing its job at compile time. While link time optimization is becoming more popular, compile time optimization still has a head start, and we should keep that in mind.
Splitting up translation units
With the rule that a source file should normally have a single associated header, we have a natural separation of our code in the different compilation units. However, the question remains how we should separate our code into those units.
Looking at other languages, e.g. Java, there can be very simple rules. Java simply demands that each class be implemented in its own source file, with the file having the same name as the class.
For normal C++ classes and class templates that seems like a reasonable rule, too. However, C++ has other entities that may not deserve their own translation unit but are not part of a class either. Think of free functions and small helper classes, e.g. traits and other metaprogramming artifacts.
Free functions often belong to a single class they are working with. They belong to the extended interface of that class. The most obvious example would be the stream output `operator<<`, which belongs to the type of its second argument. Such functions should naturally be declared and implemented in the same translation unit as the class they belong to.
Independent free functions, traits and similar micro classes usually come in groups. If we can find a name for such a group, it may be a good name for the header or translation unit containing it.
If we encounter an entity that does not belong to a group or if we can’t find a good name for the group, we can still give it its own translation unit. After all, it’s better to have a header with five lines of code and a descriptive name than hiding a function in some
XYZHelper.h from our colleagues who will then implement it again elsewhere.
When organizing your code into header and source files, keep it simple and predictable. Trying fancy things or being too lazy to create new files can hurt in the long run.
Next week I’ll write about source file organization on the larger scale: directories and namespaces.