How to implement a large library as header-only

Ideally, a header-only library is implemented using a single header. That is, the user only has to copy a single header to their source code to use the library. The problem with this approach is that, for really big projects, a single header can get really large. A great example of this is a popular JSON library for C++ located here: https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp.

At the time of writing, the preceding library is more than 22,000 lines of code. Attempting to make modifications to a file that is 22,000 lines of code would be awful (if your editor could even handle it). Some projects overcome this problem by implementing their header-only library using several header files with a single header file that includes the individual header files as needed (for example, Microsoft's Guideline Support Library for C++ is implemented this way). The problem with this approach is that the user must copy and maintain multiple header files, which starts to defeat the purpose of a header-only library as its complexity increases.

Another way to handle this problem is to use something such as CMake to autogenerate a single header file from multiple header files. For example, in the following, we have a header-only library with the following headers:

#include "config.h"

namespace library_name
{
void my_api()
{
if (config::show_hex) {
std::cout << std::hex << "The answer is: " << 42 << ' ';
}
else {
std::cout << std::dec << "The answer is: " << 42 << ' ';
}
}
}

As shown in the preceding code snippet, this is the same as our configuration example, with the exception that the configuration portion of the example has been replaced with an include to a config.h file. We can create this second header file as follows:

namespace library_name
{
namespace config
{
inline bool show_hex = false;
}
}

This implements the remaining portion of the example. In other words, we have split our header into two headers. We can still use our headers as follows:

#include "apis.h"

int main(void)
{
library_name::my_api();
return 0;
}

However, the problem is that users of our library would need a copy of both headers. To remove this problem, we need to autogenerate a header file. There are many ways to do this, but the following is one way to do so with CMake:

file(STRINGS "config.h" CONFIG_H)
file(STRINGS "apis.h" APIS_H)

list(APPEND MY_LIBRARY_SINGLE
"${CONFIG_H}"
""
"${APIS_H}"
)

file(REMOVE "my_library_single.h")
foreach(LINE IN LISTS MY_LIBRARY_SINGLE)
if(LINE MATCHES "#include "")
file(APPEND "my_library_single.h" "// ${LINE} ")
else()
file(APPEND "my_library_single.h" "${LINE} ")
endif()
endforeach()

The preceding code reads both headers into CMake variables using the file() function. This function converts each variable into a CMake list of strings (each string is a line in the file). Then, we combine both files into a single list. To create our new, autogenerated single header file, we loop through the list and write each line to a new header called my_library_single.h. Finally, if we see a reference to a local include, we comment it out to ensure that there are no references to our additional headers.

Now, we can use our new single header file as follows:

#include "my_library_single.h"

int main(void)
{
library_name::my_api();
return 0;
}

Using the preceding method, we can develop our library using as many includes as we like and our build system can autogenerate our single header file, which will be used by the end user, giving us the best of both worlds.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset