On the intermixing of source and header files, it doesn't quite work the way you mentioned. It's not possible to just cut-paste any old public object definition into a header file without at least applying 'static' to it, and that only works where the object truly is private to each translation unit. It's not so easy e.g. to instantiate a global variable shared across the whole program this way, so 'header only' also implies 'almost certainly free of globals', which is a good sign of hygiene
edit: yikes, in this case, the implication is totally invalid
int a;
int a;
static int b;
static int b;
And it's a valid way to define a single global/static symbol called `a`/`b`.The problem is that in the header-only case defining "int a" means you can only import the header from a single source file. With "static int a" the visibility is wrong. And if you did "extern int a" linking would fail because the symbol is never assigned to a translation unit.
// libfoo_impl.c
// or in some other translation unit, such as main.c
#define LIBFOO_IMPL
#include "vendor/libfoo.h"
// main.c
#include "vendor/libfoo.h"
int main() {
foo_inc();
printf("%d\n", foo_count);
return 0;
}
// vendor/libfoo.h
#pragma once
extern int foo_count;
void foo_inc() {
foo_count += 1;
}
#ifdef LIBFOO_IMPL
int foo_count = 0;
#endif
main.c and libfoo_impl.c both include libfoo.h, which declares foo_count with the right linkage, but the variable is only defined once, in whichever translation unit defines LIBFOO_IMPL.Occasionally you'll find a header library which supports this _IMPL paradigm as an option to avoid inlining its functions in every translation unit that calls them.
An additional technical downside in this case is the code complexity in the client code, from the required modal preprocessor flags.
Header-only is the other extreme: No makefile at all. With package managers like Conan and vcpkg, using a build-system like e.g. CMake, it's possible to have very simple and short project files which are easy to integrate.
In this regard, C and C++ are a bit behind the times compared to other languages.
This is also the approach used by this library, see:
https://github.com/jeremycw/httpserver.h#example
This approach also doesn't have the 'build time problems', since the (expensive to compile) implementation code is only seen once by the compiler (unlike many C++ header-only libs, which rely on inline code)
I know C and C++ since 1992, used them across multiple OSes, hardly seen anything that would motivate the fashion of header only files.
#define HTTPSERVER_IMPL
prior to the inclusion, then the header provides the implementation too, otherwise only the interface. Obviously, you must have this HTTPSERVER_IMPL followed by #include "httpserver.h" in only one file in your program.If someone doesn't like it, they can split the file into two: the header proper and the "impl" part in a .c file.
Just look for the part starting with #ifdef HTTPSERVER_IMPL. Take that out through to the closing #ifdef and put it into a httpserver.c file. Then put #include "httserver.h" into the top of that file, and remove all *_IMPL preprocessor cruft from httpserver.c. Now you have a proper single-header, single C file module.
As noted above, it avoids version incompatibility by essentially forcing static linking. But most developers would statically link a small two-file library anyway, so it’s a moot point. C isn’t supposed to have training wheels.
And then there came Arduino.
We really need to fix the lack of documentation / warnings for these generic C libraries. If you need to understand token-combining preprocessor magic (looking at you, kbtree) due to a lack of proper documentation, these newbies _will_ just try to wing it and _will_ stop poking as soon as it seems to work, regardless of why/how. Say hello to use-after-free & it's cousin, memory leaks.
Macros defined the individual functions themselves, in one place, (what are their names, arguments, and which library). A regular #include of these macros provided the declarations to the program. The shared lib wrapper module #defined the definition version of these macros prior to the #include, which expand to the invocation stubs.
It makes sense because the users who maintain it just have one place to add a new function; they don't have to do copy and paste programming to write the invocation wrapper.
What "module system"? The alternative would be one .c file and one .h file. It's negligible effort to add these to any project that already has more than one .c file.
Long term technical debt
Header only works for some things (particularly things that require no globals, singletons, etc) and that's a valid concern. Saying header-only = long term technical debt always (or even most of the time) feels like an assertion because I've only heard hypotheticals around why it is bad.
Unless you're using C++20 modules, you also have to deal with possibly including the header multiple times (slowing down builds), namespacing, macros potentially defined by the header, or a bunch of external/internal linkage edge cases. You only ever find out about these problems once it's too late to remove that library for a different one.
Could, should. IRL Docker became a thing mostly because of the hassle it is to do so in C.
Also, now have to prefix the hell out of it, as even anonymous namespace won't work. And not only that.
Generating rpm or deb files is hardly the end of the world, there are even package generators available.
Header only is nice and easy, dump file in the project, #include and your job is done.
That technique is used in sqlite. https://www.sqlite.org/amalgamation.html
> Combining all the code for SQLite into one big file makes SQLite easier to deploy — there is just one file to keep track of. And because all code is in a single translation unit, compilers can do better inter-procedure optimization resulting in machine code that is between 5% and 10% faster.
This header-only containing implementations adds its code to every compilation unit and the linker has to be smart enough to deduplicate identical symbols across many compilation units. Linkers (ld command) are provided either by the platform vendor or the compiler vendor, so they may or may not be able to do this. Binutils on Linux is able to do so IIRC.
It's a terrible practice that promotes cowboy coding. It should just use two files, which would cause less problems in the real world.
You can also write C without headers or newlines. IOCCC may welcome it but why would you ship it or encourage people to use it?
That's not how this works. It's actually a pretty clever trick, and it effectively behaves as a .h / .c pair of files. See https://github.com/nothings/stb/blob/master/docs/stb_howto.t...
Knowing why I have to `#define FOO_IMPL` before I include the header, but only from one of my compilation units, is a “clever trick” that now anybody reading or maintaining my code has to tuck away into their brain as well. Along with all the other tricks they’ve had to memorize because some other asshole thought they were being clever too.
It's a symptom of the tooling that this is even a thing.
As a library author you basically have to wait for distributions to start shipping it, or make users manually install dependencies (and then make the build work with libraries installed into /usr/local).
That's assuming the distro even does package it up. Because beyond Archlinux packaging, packaging up a DEB is painful. RPM is somewhat less painful, but you still need to contend with N distros. Your lib depends on lets say 4 other libraries, perhaps some of the GTK ecosystem of libraries. Good Luck!
When you go to build C projects, there's usually a long list of instructions, per distro/os to set the environment up correctly.
Compare that to a Rust project where you git clone and cargo build to get going. No instructions needed, no insanely complicated esoteric m4 macro system, or ad-hoc programming language (looking at you CMake), and snowflake package manager needed.
Meson is the only C/C++ build tool I know of that solves the issue of building complex C/C++ programs with dependencies sanely. Especially in the case where both your application and your dependencies are unreleased as of yet, as is the case with Gnome and KDE applications as they are developed.
Shudder
Or even plain old tarball with any sort of build script.
And vcpkg, cargo are already good options for those that need cross-os package manager.
says who? care to elaborate?
Not knocking the overall effort here, but yes, "header only" libs are definitely an anti-pattern in real world C. I assume the author considers it a curiosity, a novelty, or something that would only be used in very limited circumstances that make such a design an advantage.
This can be avoided by being careful to only define HTTP_BODY after including httpserver.h, but avoiding this type of thing to worry about is the entire point of interface/implementation separation.
What you are talking about is not an issue in practice.
// httpserverlib.c
#define HTTPSERVER_IMPL
#include "httpserver.h"
// TODO: look up definition of "compilation unit"Even windows.h redefines min and max - blaming single file libraries is ridiculous.
Using a single .c file that includes the file and defines the IMPL symbol would negate what you are saying anyway and in fact is a common way to use them.
Ok, well we're obviously arguing semantics at this point, then, because in my opinion, moving the IMPL definition into a single .c file doesn't negate the complaint about single-header libraries, it makes it no longer a single-header library. :D
GCC [0], Clang [1] and others [2] have supported for compiled headers. Cmake also has support for precompiled headers [3].
I would say both the tool and architecture to do that is well supported.
[0] https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
[1] https://clang.llvm.org/docs/PCHInternals.html
[2] https://www.qnx.com/developers/docs/6.3.2/neutrino/utilities... (-pch flag)
[3] https://cmake.org/cmake/help/latest/command/target_precompil...
It is soon 2020, there is no reasons to use C in new code. Because of security and also convenience.
Maybe it is a nice mental exercise (also try brainfuck and malbolge), but not something for production.