Header-only libraries are exactly as they sound; an entire library is implemented using header files (usually a single header file). The benefit of header-only libraries is that they are easy to include in your project as you simply include the header and you are done (there is no need to compile the library as there are no source files to compile). In this recipe, we will learn about some issues that arise when attempting to create a header-only library and how to overcome them. This recipe is important because, if you plan to create your own library, a header-only library is a great place to start and will likely increase your adoption rates as downstream users will have less trouble integrating your library into their code base.
Header-only libraries
Getting ready
As with all of the recipes in this chapter, ensure that all of the technical requirements have been met, including installing Ubuntu 18.04 or higher and running the following in a Terminal window:
> sudo apt-get install build-essential git cmake
This will ensure your operating system has the proper tools to compile and execute the examples in this recipe. Once you have done this, open a new Terminal. We will use this Terminal to download, compile, and run our examples.
How to do it...
You need to perform the following steps to complete this recipe:
- From a new Terminal, run the following to download the source code:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter01
- To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe03_examples
- Once the source code has been compiled, you can execute each example in this recipe by running the following commands:
> ./recipe03_example01
The answer is: 42
> ./recipe03_example02
The answer is: 42
> ./recipe03_example03
The answer is: 42
> ./recipe03_example04
The answer is: 42
The answer is: 2a
> ./recipe03_example05
> ./recipe03_example06
The answer is: 42
> ./recipe03_example07
The answer is: 42
In the next section, we will step through each of these examples and explain what each example program does and how it relates to the lessons being taught in this recipe.
How it works...
To create a header-only library, simply ensure that all of your code is implemented in header files, as follows:
#ifndef MY_LIBRARY
#define MY_LIBRARY
namespace library_name
{
int my_api() { return 42; }
}
#endif
The preceding example implements a simple library with a single function. The entire implementation of this library can be implemented in a single header file and included in our code as follows:
#include "my_library.h"
#include <iostream>
int main(void)
{
using namespace library_name;
std::cout << "The answer is: " << my_api() << '\n';
return 0;
}
Although creating header-only libraries seems simple enough, there are some issues that arise when attempting to create a header-only library that should be taken into account.
How to handle includes
In the preceding example, you might have noticed that, when we used our custom header-only library, we included the library first. This is an essential first step to writing a header-only library. When writing examples or tests for header-only libraries, our library should be the first thing we include to ensure that all of the header's dependencies are defined in the header-only library and not in our example or test.
For example, suppose we change our library as follows:
#ifndef MY_LIBRARY
#define MY_LIBRARY
namespace library_name
{
void my_api()
{
std::cout << "The answer is: 42" << '\n';
}
}
#endif
As shown in the preceding code snippet, instead of returning an integer our API now outputs to stdout. We can use our new API as follows:
#include <iostream>
#include "my_library.h"
int main(void)
{
library_name::my_api();
return 0;
}
Although the preceding code compiles and runs as expected, there is a bug in the code that would likely only be identified by the user of your library. Specifically, if the user of your library swaps the order of the includes or doesn't #include <iostream>, the code will fail to compile and produce the following error:
This is because the header-only library itself doesn't include all of its dependencies. Since our example put the library after other includes, our example accidentally hides this issue. For this reason, when creating your own header-only library, always include the library first in your tests and examples to ensure this type of issue never happens to your users.
Global variables
One of the biggest limitations with header-only libraries is that, prior to C++17, there was no way to create global variables. Although global variables should be avoided whenever possible, there are situations where they are needed. To demonstrate this, let's create a simple API that outputs to stdout as follows:
#ifndef MY_LIBRARY
#define MY_LIBRARY
#include <iostream>
#include <iomanip>
namespace library_name
{
void my_api(bool show_hex = false)
{
if (show_hex) {
std::cout << std::hex << "The answer is: " << 42 << '\n';
}
else {
std::cout << std::dec << "The answer is: " << 42 << '\n';
}
}
}
#endif
The preceding example creates an API that will output to stdout. If the API is executed with true instead of the default false, it will output integers in hexadecimal instead of decimal format. In this example, the change from decimal to hexadecimal is really a configuration setting in our library. Without global variables, however, we would have to resort to other mechanisms to make this work, including macros or, in the preceding example, function parameters; the latter choice is even worse as it couples the configuration of the library to its API, which means any additional configuration options would alter the API itself.
One of the best ways to address this is to use global variables in C++17, as follows:
#ifndef MY_LIBRARY
#define MY_LIBRARY
#include <iostream>
#include <iomanip>
namespace library_name
{
namespace config
{
inline bool show_hex = false;
}
void my_api()
{
if (config::show_hex) {
std::cout << std::hex << "The answer is: " << 42 << '\n';
}
else {
std::cout << std::dec << "The answer is: " << 42 << '\n';
}
}
}
#endif
As shown in the preceding example, we added a new namespace to our library called config. Our API no longer needs any parameters and determines how to function based on an inline global variable instead. Now, we can use this API as follows:
#include "my_library.h"
#include <iostream>
int main(void)
{
library_name::my_api();
library_name::config::show_hex = true;
library_name::my_api();
return 0;
}
The results in the following output:
It should be noted that we placed the configuration setting in a config namespace to ensure that our library's namespace isn't polluted with name collisions, which ultimately ensures that the intent of the global variable is obvious.
Issues with C-style macros
The biggest issue with C-style macros is that, if you place them in a C++ namespace, their name is not decorated by the namespace. This means that macros always pollute the global namespace. For example, suppose you are writing a library that needs to check the value of a variable, as follows:
#ifndef MY_LIBRARY
#define MY_LIBRARY
#include <cassert>
namespace library_name
{
#define CHECK(a) assert(a == 42)
void my_api(int val)
{
CHECK(val);
}
}
#endif
As shown in the preceding code snippet, we have created a simple API that uses a C-style macro to check an integer value in its implementation. The problem with the preceding example is that, if you attempt to use a unit test library with your own library, you will likely end up with a namespace collision.
C++20 could fix this using C++20 modules and is a topic we will discuss in more detail in Chapter 13, Bonus – Using C++20 Features. Specifically, C++20 modules do not expose C-style macros to the user of the library. The positive side of this is you will be able to use macros without namespace issues as your macros will not be exposed to the user. The downside to this approach is that a lot of library authors use C-style macros to configure a library (for example, they define a macro prior to including the library to change its default behavior). This type of library configuration will not work with C++ modules unless the macros are defined on the command line when the library is compiled.
Until C++20 is available, if you need to use macros make sure you manually add decorations to the macro names, as follows:
#define LIBRARY_NAME__CHECK(a) assert(a == 42)
The preceding line of code would do the same thing as having the macro were inside the C++ namespace, ensuring your macro doesn't collide with macros from other libraries or macros the user might define.
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 << '\n';
}
else {
std::cout << std::dec << "The answer is: " << 42 << '\n';
}
}
}
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}\n")
else()
file(APPEND "my_library_single.h" "${LINE}\n")
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.