Building a header-only library — Hello World

Ryan Graham
5 min readApr 26, 2020

--

Installing a header-only library

I recently set out to publish my first header-only library, but I found most of the existing examples to be extremely dense. They were all well maintained and supported dozens of linters, formatters, and package managers. Some were even skeleton projects for quickly stamping out the boilerplate necessary to create a new library. I could have used hpp-skel as my template and been off to the races adding my own code. But I wanted to understand every line of code in my new repository and to do that I knew I would need to start small. I needed hello world.

If you’re in the same boat this article is for you.

Directory Structure

I’ve observed two common layouts. Anything is possible with cmake, but these two archetypes hold true in most cases.

The first is the single header structure which features an include folder at the root of the repository with a single header file named after the library itself. Consumers of the library can include that file once and it will include all of the necessary implementation files. I tend to lean toward this structure because the projects I am working on right now are mostly small and closely coupled enough that you need to include the entire code base. Programs using hello need only #include <hello.hpp> and they get access to everything. This is what the directory structure looks like in hello.

➜ tree -I 'examples|cmake' hello
hello
├── CMakeLists.txt
├── Makefile
├── include
│ ├── hello
│ │ └── greeting.hpp
│ └── hello.hpp
└── test
└── test.cpp

3 directories, 5 files

The second common layout is a nested multi-header structure which flattens the dependency tree and allows consumers to include individual headers. In the following example you’ll see that programs using type_safe can pick and choose which files to include as long as they prefix the filename with type_safe/.

➜ tree type_safe/include -I detail
type_safe/include
└── type_safe
├── arithmetic_policy.hpp
├── boolean.hpp
├── bounded_type.hpp
├── compact_optional.hpp
├── config.hpp
├── constrained_type.hpp
├── deferred_construction.hpp
├── downcast.hpp
├── flag.hpp
├── flag_set.hpp
├── floating_point.hpp
├── index.hpp
├── integer.hpp
├── narrow_cast.hpp
├── optional.hpp
├── optional_ref.hpp
├── output_parameter.hpp
├── reference.hpp
├── strong_typedef.hpp
├── tagged_union.hpp
├── types.hpp
├── variant.hpp
└── visitor.hpp
1 directory, 23 files

If you’re planning to follow along, now is your chance to build your own directory structure. It might look something like this initially.

mkdir hello
cd hello
git init
mkdir test
mkdir -p include/hello

Just the code, Thanks

Let’s start with our implementation file.

// include/hello/greeting.hpp#include <string>namespace hello {inline std::string greeting()
{
std::string response = "Hello World!!";
return response;
}
}

Next create the main interface header file which includes all implementation files.

// include/hello.hpp#include "hello/greeting.hpp"

And while we are here let’s write a test as well.

// test/test.cpp#include <hello.hpp>
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
TEST_CASE("test_greeting")
{
std::string value = hello::greeting();
REQUIRE(value == std::string("Hello World!!"));
}

Thats it. Next up is the hard part for those new to cmake. Getting it to build.

Building

We will start with just enough cmake to build the project.

# CMakeLists.txt - Part 1# Build
cmake_minimum_required(VERSION 3.12)
project(hello VERSION 1.0.0 LANGUAGES CXX)include(GNUInstallDirs)add_library(${PROJECT_NAME} INTERFACE)target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_11)target_include_directories(
${PROJECT_NAME}
INTERFACE
$<BUILD_INTERFACE:${${PROJECT_NAME}_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Now try building under /build/ which should be git ignored.

mkdir build
cd build
cmake ..
make

Testing

Start by installing Catch2 if you don’t already have it. Then we will show cmake how to discover catch tests.

# CMakeLists.txt - Part 2# Test
find_package(Catch2 REQUIRED)
add_executable(hello_test test/test.cpp)
target_link_libraries(hello_test PRIVATE Catch2::Catch2)
target_include_directories(hello_test PRIVATE ${PROJECT_SOURCE_DIR}/include)
include(CTest)
include(Catch)
catch_discover_tests(hello_test)
enable_testing()

Try running tests.

cd build
cmake ..
make
make test

Installing

This part could theoretically work with about 2 lines, but I also want it to work with find_package, which you’ll encounter later. And to do that things get a bit verbose.

First our config.

# cmake/helloConfig.cmake.in@PACKAGE_INIT@include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")set_and_check(hello_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")
check_required_components("@PROJECT_NAME@")

Then the final section of CMakeLists.txt.

# CMakeLists.txt - Part 3# Install
install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}_Targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_NAME}ConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion)
if(NOT INCLUDE_INSTALL_DIR)
set(INCLUDE_INSTALL_DIR include/hello)
endif()
configure_package_config_file(
"${PROJECT_SOURCE_DIR}/cmake/${PROJECT_NAME}Config.cmake.in"
"${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake
PATH_VARS INCLUDE_INSTALL_DIR)
install(EXPORT ${PROJECT_NAME}_Targets
FILE ${PROJECT_NAME}Targets.cmake
NAMESPACE ${PROJECT_NAME}::
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake)
install(FILES "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
"${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})

I’m not even going to try to break that down line by line. Let’s just try it out so you can see what it does.

cd build
cmake ..
cmake --build . --config Release --target install
---
-- The CXX compiler identification is AppleClang 11.0.0.11000033
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/ryangraham/ryan/hello/build
Scanning dependencies of target hello_test
[ 50%] Building CXX object CMakeFiles/hello_test.dir/test/test.cpp.o
[100%] Linking CXX executable hello_test
[100%] Built target hello_test
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/share/hello/cmake/helloTargets.cmake
-- Installing: /usr/local/share/hello/cmake/helloConfig.cmake
-- Installing: /usr/local/share/hello/cmake/helloConfigVersion.cmake
-- Up-to-date: /usr/local/include/hello
-- Installing: /usr/local/include/hello/hello.hpp
-- Up-to-date: /usr/local/include/hello/hello
-- Installing: /usr/local/include/hello/hello/greeting.hpp

Usage

There are a few different ways to skin this cat. But let’s keep it simple. We’re going to build a command line app that uses our library to print out hello world. It will use a cmake builtin called find_package to locate our library where we previously installed it on our system rather than supplying an include path or copying our lib into our new directory structure. Speaking of directory structure, this is what it will look like.

➜ tree hello/examples
hello/examples
└── cli
├── CMakeLists.txt
├── Makefile
└── src
└── main.cpp
2 directories, 3 file

Then our code. Note that the header file needs no path prefix. This is because find_package will supply the correct include directory.

// cli/src/main.cpp#include <hello.hpp>
#include <iostream>
int main()
{
using namespace hello; std::string value = greeting();
std::cout << value << std::endl;
return 0;
}

And our build scripts. The message calls are unnecessary, but I found them useful while I was wrapping my head around the interaction between this file and the config for the installed library.

# cli/CMakeLists.txtcmake_minimum_required(VERSION 3.12)
project(cli LANGUAGES CXX)
find_package(hello CONFIG REQUIRED)
message("hello_DIR: ${hello_DIR}")
message("hello_INCLUDE_DIR: ${hello_INCLUDE_DIR}")
include_directories(${hello_INCLUDE_DIR})
add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} hello::hello)
add_custom_target(run
COMMAND cli
DEPENDS cli
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)

Then test it out.

# starting in cli as pwdmkdir build
cd build
cmake ..
make
make run
---
-- The CXX compiler identification is AppleClang 11.0.0.11000033
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
hello_DIR: /usr/local/share/hello/cmake
hello_INCLUDE_DIR: /usr/local/include/hello
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/ryangraham/ryan/hello/examples/cli/build
Scanning dependencies of target cli
[ 50%] Building CXX object CMakeFiles/cli.dir/src/main.cpp.o
[100%] Linking CXX executable cli
[100%] Built target cli
[100%] Built target cli
Scanning dependencies of target run
Hello World!!
[100%] Built target run

Conclusion

You shouldn’t use any of this as a blueprint for what good looks like. But hopefully it can help with wrapping your head around the basic building blocks before you get started with something more advanced.

If you want to browse the code again, check it out on Github.

--

--