![]() |
XTL
0.1
eXtended Template Library
|
The modern buzz for library development is a 'header-only' format in which the goal is to provide the code in inline headers rather than separating declarations and definitions into separate files. Taking this one step further I wish to submit that the benefits of header-only designs extend to application development as well and is a wise practice to implement. The purpose of separating declarations from definitions in classic C++ was for resource limitations in computers during an era that has long past. Old habits die hard and it's no longer necessary. Arguably, separating code into .h and .c or .hpp and .cpp files is a bad practice by today's standards.
The basic C++ compilation process passes individual implementation units through the preprocessor to generate a temporary compilation unit. The compilation unit is then passed through the compiler to produce an intermediate object. Typically, all the applications .c/.cpp files are processed this way to produce a number of intermediate files which are finally collected by the linker to produce the resulting binary. It was necessary in ancient times to break down compilation steps this way because systems simply didn't have enough resources to compile all the code in a project.
Each file that's included in a compilation unit must pass through the tokenizer and parser before reaching the compiler. Tokenizing and parsing can account for a good majority of compile times depending on the project so precompiled headers were introduced to help out. PCH is a solution to a problem that was created as the result of a solution to another problem which no longer exists. And though PCH can help improve compile times in binaries with separate declarations and definitions, neither are necessary by today's standards.
I submit that the best format for today's application code is to have a single .cpp file which contains the application's entry point and all other code is contained in inline headers. Adopting this practice is a bit strange at first but it quickly becomes preferable to the classic format with the gains that are realized.
Reduced code is always a win. When all the code is contained in inline headers then the declarations are eliminated. This amounts to reduced technical debt. If an interface or parameter needs to change then it only needs to change in one place.
With the reduction in code comes the reduction in files, half of them to be exact. There's no longer a need to bounce around between multiple files to modify a single logical unit of code. Classic C++ suggested declarations in one file and definitions in another. Half the file maintenance is a big win in the technical debt department.
Compile times can be significantly faster than separating declarations/defintions. It also builds faster than a PCH enabled binary. PCH works by caching a copy of the AST to disk after a compilation unit has passed through the tokenizer and parser then re-using the AST when another compilation unit requests the same headers. This skips the tokenizing and parsing passes of code that has already been encountered. A single compilation unit gets tokenized, parsed and passed through the compiler only once so there's no room for PCH to improve on it. Enabling PCH on such a binary would add an additional step of writing out the AST cache to disk which would never be used anyway.
Increased productivity comes with less code, less files and faster compile times. The bean counters are always happy about increased output. But, increasing productivity will also be gained from the faster binaries that result from the build.
A final relic which is no longer needed is 'link time code generation' which is another 'fix' to the first problem which no longer exists. Multiple compilation units mean inefficiencies in the generated intermediate code. Modern linkers will try to get around some of these inefficiencies by utilizing LTCG to recompile portions of the intermediate objects. Many inefficiencies are reduced with LTCG but many more can still exist and wind up in the resulting executable binary. The most efficient binary results from giving everything to the compiler in a single compilation unit. When the compiler has complete visibility into all the executable code it will more aggressively optimize and the size of the resulting binary is often smaller too.