Building a header-only library — Hello World

Image for post
Image for post
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

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

// 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

# 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

# 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

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

➜ 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

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

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store