Creating a Header-Only Library with CMake

It seems like header-only libraries are all the rage in C++ these days. In this post, we'll see a useful way to organize your header only library and generate a CMake target that others can "link" to. We will make use of CMake's INTERFACE library feature to do this:

An INTERFACE library target does not directly create build output, though it may have properties set on it and it may be installed, exported and imported.

Hopefully by the end of this post you will understand why this is so useful!

The Directory Structure

There are a few options here, but in general it makes sense to have an include directory for all the header files in your library. You may then place your headers directly in the include directory, as in cxxopts. Or you may choose to create an additional subdirectory with the same name as your library's namespace, as in type_safe. So for a project that links with your library, a developer can #include in one of two ways:

1
2
#include <cxxopts.hpp>
#include <type_safe/strong_typedef.hpp>

Examples and test files should go into their own directory and not pollute the include directory. This provides two benefits:

  1. It is easy to use CMake's target_include_directories feature
  2. It is easy to install the include directory into a destination

Of course, this is not set in stone. For example, nlohmann's json library places its header files in a src directory. But tests and benchmarks are still relegated to their own directories.

Creating a Header-Only CMake Target

Creating an interface library in CMake is very straightforward:

1
add_library(my-library-name INTERFACE)

By specifying INTERFACE as the second parameter to add_library, we are no longer allowed to provide source files since the library is not meant to generate any build output. However, if you would like your library to be IDE friendly, then you can specify the source files using target_sources, which is done by the type_safe library. target_sources was added in CMake 3.1 and allows headers that are not part of the executable target to be displayed in IDEs. (There are also other benefits to target_sources).

Once you've created a target for your header-only library, you should at the very least specify its target_include_directories. If you followed the directory structure above, then it is as simple as:

1
target_include_directories(my-library INTERFACE include/)

We specify INTERFACE again here so that future targets that link with our library will inherit our library's include directory. This is extremely convenient and is the main reason we are creating an INTERFACE library. We can continue to use INTERFACE when using target_link_libraries, target_compile_definitions, etc. so that we can propagate dependencies to targets that link with our library. In other words, the only requirement another developer has in using your library is to:

1
target_link_library(another-users-target my-library)

Doing so will ensure that any INTERFACE-marked include directories, compile definitions, other linked libraries, etc. are also added to another-users-target.

INTERFACE versus PUBLIC versus PRIVATE

It may be handy to understand how INTERFACE, PUBLIC, and PRIVATE behave. If PRIVATE is specified for a certain option/property, then that option/property will only impact the current target. If PUBLIC is specified, then the option/property impacts both the current target and any others that link to it. If INTERFACE is specified, then the option/property does not impact the current target but will propagate to other targets that link to it.

This makes INTERFACE very useful for header-only libraries that aren't compiled (on their own), but rely on other libraries they need to link to. Using target_link_library on our header-only library with INTERFACE specified means that the executable that eventually gets compiled with our library will have the appropriate link flags.