I've tried to find an answer to this using SO. There are a number of questions that list the various pros and cons of building a header-only library in c++, but I haven't been able to find one that does so in quantifiable terms.
So, in quantifiable terms, what's different between using traditionally separated c++ header and implementation files versus header only?
For simplicity, I'm assuming that templates are not used (because they require header only).
To elaborate, I've listed what I have seen from the articles to be the pros and cons. Obviously, some are not easily quantifiable (such as ease of use), and are therefore useless for quantifiable comparison. I'll mark those that I expect quantifiable metrics with a (quantifiable).
Pros for header-only
Cons for header-only
Any examples that you can use from larger, open source projects (comparing similarly-sized codebases) would be very much appreciated. Or, if you know of a project that can switch between header-only and separated versions (using a third file that includes both), that would be ideal. Anecdotal numbers are useful too because they give me a ballpark with which I can gain some insight.
sources for pros and cons:
Thanks in advance...
UPDATE:
For anyone that may be reading this later and is interested in getting a bit of background information on linking and compiling, I found these resources useful:
UPDATE: (in response to the comments below)
Just because answers may vary, doesn't mean that measurement is useless. You have to start measuring as some point. And the more measurements you have, the clearer the picture is. What I'm asking for in this question is not the whole story, but a glimpse of the picture. Sure, anyone can use numbers to skew an argument if they wanted to unethically promote their bias. However, if someone is curious about the differences between two options and publishes those results, I think that information is useful.
Has no one been curious about this topic, enough to measure it?
I love the shootout project. We could start by removing most of those variables. Only use one version of gcc on one version of linux. Only use the same hardware for all benchmarks. Do not compile with multiple threads.
Then, we can measure:
Summary (notable points):
Box2D benchmark, data:
Botan benchmark, data:
Box2D SUMMARY (78 Units)
Botan SUMMARY (301 Units)
NICE CHARTS:
Box2D executable size:
Box2D compile/link/build/run time:
Box2D compile/link/build/run max memory usage:
Botan executable size:
Botan compile/link/build/run time:
Botan compile/link/build/run max memory usage:
TL;DR
The projects tested, Box2D and Botan were chosen because they are potentially computationally expensive, contain a good number of units, and actually had few or no errors compiling as a single unit. Many other projects were attempted but were consuming too much time to "fix" into compiling as one unit. The memory footprint is measured by polling the memory footprint at regular intervals and using the maximum, and thus might not be fully accurate.
Also, this benchmark does not do automatic header dependency generation (to detect header changes). In a project using a different build system, this may add time to all benchmarks.
There are 3 compilers in the benchmark, each with 5 configurations.
Compilers:
Compiler configurations:
-O3 -march=native
-Os
-O3 -flto -march=native
with clang and gcc, -O3 -ipo -march=native
with icpc/icc-Os
I think these each can have different bearings on the comparisons between single-unit and multi-unit builds. I included LTO/IPO so we might see how the "proper" way to achieve single-unit-effectiveness compares.
Explanation of csv fields:
Test Name
- name of the benchmark. Examples: Botan, Box2D
.Test Name
.Compiler
- name of the compiler used. Examples: gcc,icc,clang
.Compiler Configuration
- name of a configuration of compiler options used. Example: gcc opt native
Compiler Version String
- first line of output of compiler version from the compiler itself. Example: g++ --version
produces g++ (GCC) 4.6.1
on my system.Header only
- a value of True
if this test case was built as a single unit, False
if it was built as a multi-unit project.Units
- number of units in the test case, even if it is built as a single unit.Compile Time,Link Time,Build Time,Run Time
- as it sounds.Re-compile Time AVG,Re-compile Time MAX,Re-link Time AVG,Re-link Time MAX,Re-build Time AVG,Re-build Time MAX
- the times across rebuilding the project after touching a single file. Each unit is touched, and for each, the project is rebuilt. The maximum times, and average times are recorded in these fields.Compile Memory,Link Memory,Build Memory,Run Memory,Executable Size
- as they sound.To reproduce the benchmarks:
"units"
- a list of c/cpp/cc
files that make up the units of this project"executable"
- A name of the executable to be compiled."link_libs"
- A space separated list of installed libraries to link to."include_directores"
- A list of directories to include in the project."command"
- optional. special command to execute to run the benchmark. For example, "command": "botan_test --benchmark"
test_base_cases
in run.py with the information for the project, including the data file name.data.csv
should contain the benchmark results.To produce the bar charts:
fields
list to decide which graphs to produce.python chart.py data.csv
.test.png
should now contain the result../configure.py --disable-asm --with-openssl --enable-modules=asn1,benchmark,block,cms,engine,entropy,filters,hash,kdf,mac,bigint,ec_gfp,mp_generic,numbertheory,mutex,rng,ssl,stream,cvc
, this generates the header files and Makefile.grep -o "\./src.*cpp" Makefile
and grep -o "\./checks.*" Makefile
to obtain the .cpp units and put them into botan_bench.data file./checks/checks.cpp
to not call the x509 unit tests, and removed x509 check, because of conflict between Botan typedef and openssl.Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz