Using CMake to create header-only, shared and static libraries

by Jan Gabriel | 19 Mar 2021

last edited: 25 Mar 2021

CMake C++ Libraries


It's important to keep in mind how you want to package and distribute your code when developing a library. You need to make design decisions such as building a header-only, static, or shared library. In this post, I'll show how you can use CMake to package your C++ library in these three ways:

  • Header-only (INTERFACE)
  • Static; and
  • Shared (dynamic)

The code for this post is available as a gist.

CMake Library types

CMake provides a comprehensive explanation of the add_library function on their help page. I summarise them below, but for this post the Normal and Interface libraries are the most important.

Normal libraries

The normal libraries include STATIC, SHARED and MODULE libraries.

Static libraries allow you to encapsulate a library within your executable, which means you can distribute your executable as a single package. This has some speed advantages, but when libraries get updated (as is their nature), you will need to rebuild and redeploy your entire executable. They are easy to spot with *.a (Linux) and *.lib (Windows) extensions.

Shared/Dynamic libraries allow you to link your library at runtime. This means you can deploy updates to your program without changing the main executable. It also has the risk of breaking the executable when something goes wrong with the library or when it is not found. They are easy to spot with *.so (Linux) and *.dll (Windows) extensions.

For some further reading on dynamic and static libraries, you can see this medium post and also this one.

Module libraries are plugins that may be loaded during runtime. It is important to note that when a library does not export any symbols, it must not be declared SHARED but rather as a MODULE library. I'll show how to export symbols in the example project for Windows.

Interface Libraries

An INTERFACE library does not create build output, because it is associated with a header-only library. The advantage is that you can still set properties on it, install it, export and import the library to other targets.

You can call target_include_directories() with the header-only directory on your target, OR rather use an interface library and import it with target_link_libraries() like any other target.

You can also use the INTERFACE library in combination with a MODULE library as shown here for creating modular plugins.

Imported Libraries

The IMPORTED libraries target is used when the library file is not built by your project and must thus be "imported". You can conveniently link them to your target with target_link_libraries().

Object Libraries

These libraries can be used when you compile source files but do not want to add them to their own library target. An object library can then be used in other targets built in the project.

Alias Libraries

These libraries provide another target with an "alias" name. You can then refer to the alias name when linking to or reading properties from.

Project setup

The previous section provided an overview of the different methods to build and add libraries to our project. In the following sections, we'll put it into practice. I'll focus on the use-case where you design a library and you've added it to a vcs repository such as Github.

Folder structure

For the purposes of this post, I've created an example library and I've added it to the main project with the following folder structure:

├─┬ lib
│ └─┬ example
│   ├──┬ include
│   │  └─┬ example
│   │    └── example.h
│   ├──┬ src
│   │  └── example.cpp
│   └── CMakeLists.txt
│
├─┬ src
│ └── main.cpp
│
└── CMakeLists.txt

Adding a library to your project from an external source

The example library is included in the main project under the lib folder. There are a few ways to get the library from a repository into your library folder. My favorite two are using:

And of course, there is always the good old download-a-zip-folder method😉.

Setting up CMake

CMakeLists.txt - Example Library

The CMakeLists.txt for the example folder shows exactly how to create our different libraries and to include their header paths. It is also important to note that we must export symbols when building a shared library on windows.

## CMakeLists.txt in example lib folder ##
cmake_minimum_required(VERSION 3.0)
project(example)

# Add INTERFACE library, thus, header-only
add_library(${PROJECT_NAME}_interface INTERFACE)
# Add target include libraries, thus, the header file include path
target_include_directories(${PROJECT_NAME}_interface INTERFACE ${PROJECT_SOURCE_DIR}/include)

# Add STATIC library with source example.cpp in src directory
add_library(${PROJECT_NAME}_static STATIC ${PROJECT_SOURCE_DIR}/src/example.cpp)
# Add target include libraries, thus, the header file include path
target_include_directories(${PROJECT_NAME}_static PUBLIC ${PROJECT_SOURCE_DIR}/include)

# Export symbols when on windows for making a SHARED lib
if(MSVC OR MINGW)
    # This is necessary for windows, requires CMake 3.4+
    cmake_minimum_required(VERSION 3.4)
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS 1)
endif()

# Add SHARED library with source example.cpp in src directory
add_library(${PROJECT_NAME}_shared SHARED ${PROJECT_SOURCE_DIR}/src/example.cpp)
# Add target include libraries, thus, the header file include path
target_include_directories(${PROJECT_NAME}_shared PUBLIC ${PROJECT_SOURCE_DIR}/include)

Side note: this StackOverflow question is also noteworthy when using CMake with MSVC when it comes to runtime library linking with /MD and /MT compiler switches. It is not required for this post, but good to know when it comes to linking libraries with CMake and MSVC.

CMakeLists.txt - Main Executable

The main executable's CMakeLists.txt file is shown below. We create three different executables where each one links to a different library.

# CMakeLists.txt in main folder
cmake_minimum_required(VERSION 3.0)
project(main)

# Add lib subdirectory (to build our libraries)
add_subdirectory(lib/example)

# Add a main executables
add_executable(${PROJECT_NAME}_header_only ${PROJECT_SOURCE_DIR}/src/main.cpp)
add_executable(${PROJECT_NAME}_shared ${PROJECT_SOURCE_DIR}/src/main.cpp)
add_executable(${PROJECT_NAME}_static ${PROJECT_SOURCE_DIR}/src/main.cpp)

# When using a header-only, thus, INTERFACE library
target_link_libraries(${PROJECT_NAME}_header_only example_interface)
# Only needed for our example #define HEADER_ONLY
target_compile_definitions(${PROJECT_NAME}_header_only PUBLIC HEADER_ONLY)

# When using a SHARED library
target_link_libraries(${PROJECT_NAME}_shared example_shared)

# When using a STATIC library
target_link_libraries(${PROJECT_NAME}_static example_static)

Source code

Example

For our example, I've added a HEADER_ONLY definition to change our Example class between a header-only (INTERFACE) implementation and a header + source (STATIC | SHARED | MODULE) implementation for the purposes of showing the differences when adding and linking the libraries to the main executable.

// example.h

#ifdef HEADER_ONLY
/*
 * The Example class takes a name as parameter during construction
 * and can print it out when its PrintName() method is called
 */
class Example {
 public:
  // Constructor
  explicit Example(std::string name) : name_(std::move(name)) {}
  // Print the name
  void PrintName() const {
    std::cout << name_ << std::endl;
  }

 private:
  const std::string name_;
};
#else
/*
 * The Example class takes a name as parameter during construction
 * and can print it out when its PrintName() method is called
 */
class Example {
 public:
  // Constructor
  explicit Example(std::string name);
  // Print the name
  void PrintName() const;

 private:
  const std::string name_;
};
#endif

// example.cpp (for STATIC + SHARED implementation)
Example::Example(std::string name) : name_(std::move(name)) {}

void Example::PrintName() const {
  std::cout << name_ << std::endl;
}

Main Executable

Our main executable includes the library, creates an Example class, and then calls its PrintName() method.

#include "example/example.h"

int main() {
  Example example("World!");

  std::cout << "Hello, ";
  example.PrintName();

  return 0;
}

Summary

This post touched on the subject of building a C++ library with CMake. The targets built by this project are the main executable in three flavors using the:

  • example header only INTERFACE library;
  • example STATIC library; and
  • example SHARED library.

The use-case example project showed the implementation of the abovementioned three different types of libraries, setting up their relevant CMakeLists.txt files and, finally, including the library for use in the main executable's source file.

I hope this post has helped you package and structure your library. If you have any questions feel free to get in touch.

Happy Coding.