Advanced C++ Programming Cookbook

3 (1 reviews total)
By Dr. Rian Quinn
  • Instant online access to over 8,000+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Getting Started with Library Development

About this book

If you think you've mastered C++ and know everything it takes to write robust applications, you'll be in for a surprise. With this book, you'll gain comprehensive insights into C++, covering exclusive tips and interesting techniques to enhance your app development process.

You'll kick off with the basic principles of library design and development, which will help you understand how to write reusable and maintainable code. You'll then discover the importance of exception safety, and how you can avoid unexpected errors or bugs in your code. The book will take you through the modern elements of C++, such as move semantics, type deductions, and coroutines. As you advance, you'll delve into template programming - the standard tool for most library developers looking to achieve high code reusability. You'll explore the STL and learn how to avoid common pitfalls while implementing templates. Later, you'll learn about the problems of multithreaded programming such as data races, deadlocks, and thread starvation. You'll also learn high-performance programming by using benchmarking tools and libraries. Finally, you'll discover advanced techniques for debugging and testing to ensure code reliability.

By the end of this book, you'll have become an expert at C++ programming and will have gained the skills to solve complex development problems with ease.

Publication date:
January 2020
Publisher
Packt
Pages
454
ISBN
9781838559915

 

Getting Started with Library Development

In this chapter, we will cover some useful recipes for creating our own libraries, including an explanation of the principle of least surprise, which encourages us to implement libraries using semantics that our users are already familiar with. We will also look at how to namespace everything to ensure our custom libraries don't conflict with others. In addition, we will look at how to create header-only libraries, as well as some best practices associated with library development. Finally, we will conclude this chapter with a demonstration of the boost libraries to show you what a large library looks like and how it can be used by users in their own projects.

In this chapter, we will cover the following recipes:

  • Understanding the principle of least surprise
  • How to namespace everything
  • Header-only libraries
  • Learning library development best practices
  • Learning how to use the boost APIs

Let's get started!

 

Technical requirements

To compile and run the examples in this chapter, you must have administrative access to a computer running Ubuntu 18.04 with a functional internet connection. Prior to running these examples, you must install the following packages using the following command:

> sudo apt-get install build-essential git cmake

If this is installed on any operating system other than Ubuntu 18.04, then GCC 7.4 or higher and CMake 3.6 or higher will be required.

 

Understanding the principle of least surprise

When either using existing C++ libraries or creating your own, understanding the principle of least surprise (also called the principle of least astonishment) is critical to developing source code efficiently and effectively. This principle simply states that any feature that a C++ library provides should be intuitive and should operate as the developer expects. Another way of saying this is that a library's APIs should be self-documenting. Although this principle is critically important when designing libraries, it can and should be applied to all forms of software development. In this recipe, we will explore this principle in depth. 

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've done this, open a new Terminal. We will use this Terminal to download, compile, and run our examples.

How to do it...

Perform the following steps to complete this recipe:

  1. From a new Terminal, run the following code to download the source code:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter01
  1. To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe01_examples
  1. Once the source code has been compiled, you can execute each example in this recipe by running the following commands:
> ./recipe01_example01
The answer is: 42

> ./recipe01_example02
The answer is: 42

> ./recipe01_example03
The answer is: 42

> ./recipe01_example04
The answer is: 42
The answer is: 42

> ./recipe01_example05
The answer is: 42
The answer is: 42

> ./recipe01_example06
The answer is: 42
The answer is: 42

> ./recipe01_example07
The answer is: 42

> ./recipe01_example08
The answer is: 42

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

As stated in the previous section, the principle of least surprise states that a library's APIs should be intuitive and self-documenting and this principle generally applies to all forms of software development and not just library design. To understand this, we'll look at some examples.

Example 1

Example 1 demonstrates the principle of least surprise as follows:

#include <iostream>

int sub(int a, int b)
{ return a + b; }

int main(void)
{
std::cout << "The answer is: " << sub(41, 1) << '\n';
return 0;
}

As shown in the preceding example, we have implemented a library API that adds two integers and returns the results. The problem is that we named the function sub, which most developers would associate with subtraction and not addition; although the API functions as designed, it breaks the principle of least surprise because the API's name is not intuitive. 

Example 2

Example 2 demonstrates the principle of least surprise as follows:

#include <iostream>

void add(int a, int &b)
{ b += a; }

int main(void)
{
int a = 41, b = 1;
add(a, b);

std::cout << "The answer is: " << b << '\n';
return 0;
}

As shown in the preceding example, we have implemented the same library API that we implemented in the previous exercise; it is designed to add two numbers and return the result. The issue with this example is that the API is implementing the following:

b += a;

In this example, the principle of least surprise is being violated in two different ways:

  • The add function's arguments are a and then b, even though we would write this equation as b += a, meaning that the order of the arguments is intuitively backward.
  • It is not immediately obvious to the user of this API that the result would be returned in b without reading the source code. 

A function's signature should document how the function will execute using semantics the user is already accustomed to, thus reducing the probability of causing the user to execute the API incorrectly.

Example 3

Example 3 demonstrates the principle of least surprise as follows:

#include <iostream>

int add(int a, int b)
{ return a + b; }

int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
return 0;
}

As shown in the preceding example, we're adhering to the principle of least surprise here. The API is designed to add two integers and return the result, and the API intuitively performs this action as expected. 

Example 4

Example 4 demonstrates the principle of least surprise as follows:

#include <stdio.h>
#include <iostream>

int main(void)
{
printf("The answer is: %d\n", 42);
std::cout << "The answer is: " << 42 << '\n';
return 0;
}

As shown in the preceding example, another great example of the principle of least surprise is the difference between printf() and std::cout. The printf() function requires the addition of format specifiers to output integers to stdout. There are many reasons why printf() is not intuitive:

  • To a beginner, the printf() function's name, which stands for print formatted, is not intuitive (or in other words, the function's name is not self-documenting). Other languages avoid this issue by picking more intuitive names for a print function, such as print() or console(), which do a better job of adhering to the principle of least surprise.  
  • The format specifier symbol for an integer is d. Once again, to a beginner this is unintuitive. In this specific case, d stands for decimal, which is another way of saying signed integer. A better format specifier might have been i to match the language's use of int

Contrast this with std::cout, which stands for character output. Although this is less intuitive compared to print() or console(), it is more intuitive than printf(). Furthermore, to output an integer to stdout, the user doesn't have to memorize a table of format specifiers to complete their task. Instead, they can simply use the << operator. Then, the APIs handle formatting for you, which is not only more intuitive but also safer (especially when working with std::cin as opposed to scanf()). 

Example 5

Example 5 demonstrates the principle of least surprise as follows:

#include <iostream>

int main(void)
{
auto answer = 41;

std::cout << "The answer is: " << ++answer << '\n';
std::cout << "The answer is: " << answer++ << '\n';

return 0;
}

As shown in the preceding example, the ++ operators uphold the principle of least surprise. Although a beginner would have to learn that ++ represents the increment operator, which means the variable is incremented by 1, the position of ++ with respect to the variable is quite helpful.

To understand the difference between ++variable and variable++, all the user has to do is read the code left to right as normal. When ++ is on the left, the variable is incremented and then the contents of the variable are returned. When ++ is on the right, the contents of the variable are returned and then the variable is incremented. The only issue with respect to the position of ++ is the fact that ++ on the left is generally more efficient (as the implementation doesn't require extra logic to store the value of the variable prior to the increment operation). 

Example 6

Example 6 demonstrates the principle of least surprise as follows:

#include <iostream>

int add(int a, int b)
{ return a + b; }

int Sub(int a, int b)
{ return a - b; }

int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
std::cout << "The answer is: " << Sub(43, 1) << '\n';

return 0;
}

As shown in the preceding code, we have implemented two different APIs. The first adds two integers and returns the results while the second subtracts two integers and returns the results. The issue with the subtract function is two-fold:

  • The addition function is in lowercase while the subtraction function is in uppercase. This is not intuitive and users of the APIs would have to learn which APIs are in lowercase and which are in uppercase. 
  • The C++ standard APIs are all in snake case, meaning they leverage lowercase words with the use of _ to denote a space. In general, it is better to design C++ library APIs with snake case as a beginner is more likely to find this intuitive. It should be noted that, although this is generally the case, the use of snake case is highly subjective and there are several languages that do not adhere to this guidance. The most important thing is to pick a convention and stick to it. 

Once again, ensuring your APIs mimic existing semantics ensures the user can quickly and easily learn to use your APIs, while reducing the probability of the user writing your APIs incorrectly, leading to compile errors. 

Example 7

Example 7 demonstrates the principle of least surprise as follows:

#include <queue>
#include <iostream>

int main(void)
{
std::queue<int> my_queue;

my_queue.emplace(42);
std::cout << "The answer is: " << my_queue.front() << '\n';
my_queue.pop();

return 0;
}

As shown in the preceding example, we are showing you how a std::queue can be used to add integers to a queue, output the queue to stdout, and remove elements from the queue. The point of this example is to highlight the fact that C++ already has a standard set of naming conventions that should be leveraged during C++ library development.

If you are designing a new library, it is helpful to the user of your library to use the same naming conventions that C++ has already defined. Doing so will lower the barrier to entry and provide a more intuitive API. 

Example 8

Example 8 demonstrates the principle of least surprise as follows:

#include <iostream>

auto add(int a, int b)
{ return a + b; }

int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
return 0;
}

As shown in the preceding example, we are demonstrating how the use of auto, which tells the compiler to figure out what the return type of the function is automatically, does not uphold the principle of least surprise. Although auto is extremely helpful for writing generic code, its use should be avoided as much as possible when designing a library API. Specifically, for the user of the API to understand what the inputs and outputs of the API are, the user must read the API's implementation as auto does not specify the output type. 

Example 9

Example 9 demonstrates the principle of least surprise as follows:

#include <iostream>

template <typename T>
T add(T a, T b)
{ return a + b; }

int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
return 0;
}

As shown in the preceding example, we are demonstrating a more appropriate way to uphold the principle of least surprise while simultaneously supporting generic programming. Generic programming (also called template meta-programming or programming with C++ templates) provides the programmer with a way to create an algorithm without stating the types that are being used in the algorithm. In this case, the add function doesn't dictate the input type, allowing the user to add two values of any type (in this case, the type is called T, which can take on any type that supports the add operator). Instead of returning an auto, which would not state the output type, we return a type T. Although T is not defined here as it represents any type, it does tell the user of the API that any type we input into this function will also be returned by the function. This same logic is used heavily in the C++ standard library. 

 

How to namespace everything

When creating a library, it is important to namespace everything. Doing so ensures that of the APIs provided by the library cause name collisions with the user's code or with facilities provided by other libraries. In this recipe, we will demonstrate how to do this in our own 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:

  1. 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
  1. To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe02_examples

  1. Once the source code has been compiled, you can execute each example in this recipe by running the following commands:
> ./recipe02_example01
The answer is: 42

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

C++ provides us with the ability to wrap code in a namespace, which simply adds the namespace name to all functions and variables inside the namespace code (it should be noted that C style macros are not included in the namespace and should be used with care because C macros are a preprocessor feature that does not contribute to the code's compiled syntax). To explain why we should namespace everything when creating our own libraries, we'll look at some examples.

Example 1

Example 1 demonstrates how to wrap your library's APIs in a C++ namespace :

// Contents of library.h

namespace library_name
{
int my_api() { return 42; }
// ...
}

// Contents of main.cpp

#include <iostream>

int main(void)
{
using namespace library_name;

std::cout << "The answer is: " << my_api() << '\n';
return 0;
}

As shown in the preceding example, the contents of the library are wrapped in a namespace and stored in the header (this example demonstrates a header-only library, which is an extremely useful design approach as the end user doesn't have to compile libraries, install them on his/her system, and then link against them). The library user simply includes the library header file and uses the using namespace library_name statement to unwrap the library's APIs. If the user has more than one library with the same API names, this statement can be omitted to remove any ambiguity. 

Example 2

Example 2 expands upon the previous example and demonstrates how to wrap your library's APIs in a C++ namespace header-only library while still including global variables:

// Contents of library.h

namespace library_name
{
namespace details { inline int answer = 42; }

int my_api() { return details::answer; }
// ...
}

// Contents of main.cpp

#include <iostream>

int main(void)
{
using namespace library_name;

std::cout << "The answer is: " << my_api() << '\n';
return 0;
}

As shown in the preceding example, C++17 was leveraged to create an inline global variable that is wrapped in our library's namespace. inline variables are needed as header-only libraries don't have a source file to define global variables; without the inline keyword, defining a global variable in a header would result in the variable being defined multiple times (that is, the result would be a linking error during compilation). C++17 resolved this issue by adding inline global variables, which allows a header-only library to define global variables without the need for tricky magic (such as returning a pointer to a static variable from a singleton style function).

In addition to the library's namespace , we wrapped the global variable in a details namespace. This is done to create a private place within your library in case the user of the library declares using namespace library_name. If the user does this, all of the APIs and variables that are wrapped by the library_name namespace become globally accessible within the scope of the main() function. For this reason, any private APIs or variables that are not meant to be accessible by the user should be wrapped by a second namespace (typically called details) to prevent their global accessibility. Finally, leveraging C++17's inline keyword allows us to create a global variable for use in our library while still supporting a header-only design. 

 

Header-only libraries

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.

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:

  1. 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
  1. To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe03_examples
  1. 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.

 

Learning library development best practices

When writing your own library, there are certain best practices that all library authors should adhere to. In this recipe, we will explore some higher-priority best practices and conclude with some information about a project dedicated to defining these best practices, including a registration system that provides your library with a grade as to how well it compiles. This recipe is important as it will teach you how to make the highest-quality library, ensuring a strong and vibrant user base.

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 clang-tidy valgrind

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:

  1. 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
  1. To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe04_examples
  1. Once the source code has been compiled, you can execute each example in this recipe by running the following commands:
> ./recipe04_example01 
21862

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

Every library author should ensure their library is easy to use and incorporate into their users' own projects. Doing so will ensure your users continue to use your library, resulting in a growing user base over time. Let's look at a few of these best practices.

What about warnings?

The lowest possible hanging fruit for any library author is ensuring your code compiles with as many warnings enabled as possible. Sadly, GCC does not make this process simple as there is no one warning flag to rule them all, specifically because GCC has many warning flags that are not useful for modern versions of C++ (in other words, they are, in a sense, mutually exclusive). The best place to start is with the following warnings:

-Wall -Wextra -pedantic -Werror

This turns on most of the important warnings while ensuring that any warnings that your examples or tests compile will generate an error. For some libraries, however, this will not be enough. At the time of writing, the following are the flags that Microsoft's Guideline Support Library uses:

-Wall -Wcast-align -Wconversion -Wctor-dtor-privacy -Werror -Wextra -Wpedantic -Wshadow -Wsign-conversion

One additional warning that the GSL uses is conversion warnings, which will tell you when you convert between different integer types. If you are using Clang, this process can be a lot easier as it provides -Weverything. If weeding through all of the warnings that GCC provides is too much work, one approach to solving this issue is to make sure that your library compiles with the Clang compiler with this warning turned on, which will ensure your code compiles with most of the warnings that GCC provides. This way, your users will not have trouble with your library when they have to ensure specific warnings are enabled in their code as you will have tested as many of them as possible.

Static and dynamic analysis

In addition to testing for warnings, libraries should also be tested with static and dynamic analysis tools. Once again, as an author of a library, you must assume your users might use static and dynamic analysis tools to shore up the quality of their own applications. If your library triggers these tools, your users are more likely to look for alternatives that have been tested more thoroughly.

For C++, there is a large number of tools that can be used to analyze your libraries. In this recipe, we will focus on Clang Tidy and Valgrind, which are both free to use. Let's look at the following simple example:

#include <iostream>

int universe()
{
auto i = new int;
int the_answer;
return the_answer;
}

int main()
{
std::cout << universe() << '\n';
return 0;
}

In the preceding example, we created a function called universe() that returns an integer and allocates an integer. In our main function, our universe() function output the results to stdout.

To statically analyze the preceding code, we can use CMake as follows:

set(CMAKE_CXX_CLANG_TIDY clang-tidy)

The preceding line of code tells CMake to use clang-tidy when compiling the preceding example. When we compile the code, we get the following result:

If a user of your library has turned on static analysis using Clang Tidy, this is the error they might receive, even though their code is perfectly fine. If you are using someone else's library and run into this issue, one way to overcome the problem is to include the library as a system include, which tells tools such as Clang Tidy to ignore these errors. This, however, doesn't always work as some libraries require the use of macros, which expose the library's logic to your own code, resulting in chaos. In general, if you are a library developer, statically analyze your library as much as you can afford to as you don't know how your users might use your library.

The same goes for dynamic analysis. The preceding analysis didn't detect the obvious memory leak. To identify this, we can use valgrind, as follows:

As shown in the preceding screenshot, valgrind is able to detect the memory leak in our code. Actually, valgrind also detects the fact that we never initialize our temporary variable in the universe() function, but the output is far too verbose to show here. Once again, if you fail to identify these types of problem with your libraries, you will end up exposing these bugs to your users.

Documentation

Documentation is an absolute must for any good library. Besides buggy code, a lack of documentation will absolutely prevent others from using your library. Libraries should be easy to set up and install, and even easier to learn and incorporate into your own applications. One of the most frustrating aspects of using existing C++ libraries is the lack of documentation.

CII Best Practices

In this recipe, we have touched on a couple of common best practices that all library developers should incorporate into their projects. In addition to these best practices, a more complete list of best practices is provided by the CII Best Practices program here: https://bestpractices.coreinfrastructure.org/en.

The CII Best Practices program provides a comprehensive list of best practices that are updated over time and that library developers (and any application in general) can leverage. These best practices are grouped into passing, silver, and gold, with the gold practices being the hardest to achieve. The higher your score, the more likely users are to use your library as it shows commitment and stability.

 

Learning how to use the boost APIs

The boost libraries are a set of libraries designed to work in conjunction with the standard C++ libraries. In fact, a lot of the libraries that are currently being provided by C++ originated from the boost libraries. The boost libraries provide everything from containers, clocks, and timers to more complicated mathematical APIs such as graphs and CRC calculations. In this recipe, we will learn how to use the boost libraries, specifically to demonstrate what a large library looks like and how such a library would be included in a user's project. This recipe is important as it will demonstrate just how complicated a library can get, teaching you how to write your own libraries accordingly.

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 libboost-all-dev

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:

  1. 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
  1. To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe05_examples
  1. Once the source code has been compiled, you can execute each example in this recipe by running the following commands:
> ./recipe05_example01
Date/Time: 1553894555446451393 nanoseconds since Jan 1, 1970
> ./recipe05_example02
[2019-03-29 15:22:36.756819] [0x00007f5ee158b740] [debug] debug message
[2019-03-29 15:22:36.756846] [0x00007f5ee158b740] [info] info message

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

The boost libraries provide a set of user APIs that implement commonly needed functionality in most programs. These libraries can be included in your own projects to simplify your code and provide an example of what a finished library might look like. To explain how your own libraries could be leveraged by others, let's look at some examples of how to use the boost libraries.

Example 1

In this example, we are using the boost APIs to output the current date and time to stdout, as follows:

#include <iostream>
#include <boost/chrono.hpp>

int main(void)
{
using namespace boost::chrono;

std::cout << "Date/Time: " << system_clock::now() << '\n';
return 0;
}

As shown in the preceding example, the current date and time are outputted to stdout as the total number of nanoseconds since the Unix Epoch (January 1, 1970). In addition to including boost in your source code, you must also link your application against the boost libraries. In this case, we needed to link against the following:

-lboost_chrono -lboost_system -lpthread

An example of how this is done can be seen in the CMakeLists.txt file that was downloaded with this recipe. Once these libraries have been linked to your project, your code will be able to leverage the APIs inside them. This extra step is why header-only libraries can be so useful when creating your own libraries as they obviate the need for additional linking.

Example 2

In this example, we're demonstrating how to log to the console using boost's trivial logging APIs, as follows:

#include <boost/log/trivial.hpp>

int main(void)
{
BOOST_LOG_TRIVIAL(debug) << "debug message";
BOOST_LOG_TRIVIAL(info) << "info message";
return 0;
}

As shown in the preceding example, the "debug message" and "info message" messages were outputted to stdout. In addition to linking against the proper boost libraries, we also had to include the following definition during compilation:

-DBOOST_LOG_DYN_LINK -lboost_log -lboost_system -lpthread

Once again, linking these libraries ensures that the APIs you are using in your code (as shown in the preceding example) exist in the executable. 

See also

About the Author

  • Dr. Rian Quinn

    Dr. Rian Quinn is the Chief Technology Officer (CTO) in the Advanced Technologies Business Unit at Assured Information Security, Inc., having focused on trusted computing, hypervisor-related technologies, machine learning/artificial intelligence, and cybersecurity for more than 10 years, and has 9 years of technical management and business development experience. He holds a Ph.D. in computer engineering, with specializations in information assurance and computer architectures, from Binghamton University. He is the cofounder and lead developer of the Bareflank hypervisor, and is an active member of several open source projects, including Microsoft's Guideline Support Library (GSL) and OpenXT. Rian previously wrote Hands-On System Programming with C++.

    Browse publications by this author

Latest Reviews

(1 reviews total)
Examples are very trivial

Recommended For You

Book Title
Access this book, plus 8,000 other titles for FREE
Access now