CMake and C++ on Linux

On this page we discuss how to setup a basic C++ project with CMake for scientific computing. It will use the Boost, Eigen and OpenMP libraries, to provide general functionality, linear algebra and parallel programming respectively. The main goal of this tutorial is to get you to a point where compiling, linking and running your scientific computing project will be nothing more than one or two commands on the command line, such that you can fully focus on writing code and getting results instead of wasting time on the build process.

Getting the dependencies

Before we can do anything we need to get and install all the dependencies. We need

  1. An IDE or text editor
  2. CMake
  3. Boost
  4. Eigen3

The text editor can be of your own choice a highly recommended editor is Visual Studio Code. To install the dependencies on a Ubuntu based distro run

sudo apt-get install -y make cmake libeigen3-dev libboost-all-dev

Note that you will already have all these packages installed if you use software like the VOTCA project.

Setting up a Default C++ Project

Small and medium sized C++ projects almost always have the same basic file and folder structure. There is an include directory for the headers (.h), a source directory (often called src) for the source code files (.cpp) and a build folder for the build files that are generated by CMake and optionally a bin folder where the final binary is installed.

Every C++ project starts with such a basic layout of files and folders. To get started create the following file and folder structure in some folder on your computer. I have created this example in my home directory in a folder called exampleProject.

exampleProject
├── build
├── include
├── src
│   └── main.cpp
├── .gitignore
└── CMakeLists.txt

The main.cpp, .gitignore and CMakeLists.txt can be empty text files for now.

Hello World (with C++ and CMake)

Now that we have the basic file and folder structure lets compile an example. We start off with a simple Hello World program.

Source Code

In the main.cpp text file copy paste the following code. We don’t care about what the code does or how it does it, if you want to know about that follow a basic C++ tutorial somewhere on the web. The focus here is on compiling and linking and making that as quick and painless as possible.

#include <iostream>

int main() {
    std::cout << "Hello World!" << std::endl;
    return 0;
}

CMakeLists.txt

Now that we have some code to compile, let’s look at how we can use CMake to do this. What we need to do is create a CMakeLists.txt file, if you followed the instructions above you have already made it. Copy paste the following code into that file. The example file is well commented to explain exactly what every line of CMake code does.

# Sets the minimum required version of CMake to 3.8
cmake_minimum_required (VERSION 3.8)

# sets the projects name
project ("example")

# There are many different C++ versions labeled by their release year here we
# will use the one from 2014 for compatibility with the VOTCA-XTP project. You
# are free to use a more recent C++ version if you need the features
set(CMAKE_CXX_STANDARD 14)
# If 14 is not available we want an error, this is done by setting it as
# required You will see the REQUIRED keyword more often, it is always used to
# raise an error if a specific function, module, package etc. can't be used.
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Here we specify the folders where the compiler should look for include files.
# We have created an include folder specifically for this purpose, so we add it
# here
include_directories(include)

# To make compilation easy we want CMake to look for all possible .cpp files
# that should be compiled. We do this with a "GLOB", it is a way of writing
# generic # file names. For example the GLOB "src/*.cpp" translates to every
# file in the src folder that ends with .cpp, the asterix (*) is called a wild
# card, it matches any possible string.
file(GLOB SOURCES "src/*.cpp")
# after this command all source files (.cpp) are collected in the variable
# SOURCES

# Now we create an executable (the actual program) named *example*, we want it
# to use all the source files. We need ${variable} to access the contents of a
# variable, hence ${SOURCES} gives all the sources collected with the GLOB
# expression above.
add_executable (example ${SOURCES})

# This line copies the created binaries (in this case only one) to the
# installation location. The destination here is the bin folder. Depending on
# which options CMake gets, this is either the /usr/local/bin (default) or the
# bin folder of the CMAKE_INSTALL_PREFIX option.
install(TARGETS example DESTINATION bin)

Building A CMake Project

All CMake projects are build in more or less the same way. First the project needs to be configured, that is done with the following command

cmake -Bbuild_dir -DCMAKE_INSTALL_PREFIX=./ .

This calls CMake and tells it to configure the current folder, ., as a CMake project, the configuration/build files will be written to the build directory, which is indicated with the -B flag and is called build_dir in this example. The -DCMAKE_INSTALL_PREFIX=./ option tells the program to install the binaries to the current folder ./, but you can specify any location on your computer, if you do not provide an install location, the default (/usr/local/bin) will be used. After running this command you will get all configuration info printed to the console, it can be interesting to inspect and see which compiler is used, which packages are found etc.

After configuring the project we need to actually build it. This is done using

cmake --build build_dir --parallel <nrOfThreadsToUse>

which calls CMake and tells it to build the project based on the build files in build-dir using <nrOfThreadsToUse> threads to do so. If you don’t want your code to compile quickly (which would be stupid), you can run it without the --parallel option.

Finally to install a CMake project we run

cmake --build build_dir --target install

Which will copy the binaries (and possibly other files if you specified them) to their install location.

Note that the last two commands can be combined into one, like this

cmake --build build_dir --parallel <nrOfThreadsToUse> --target install

To finally run your program navigate to the bin folder and there you find the executable to run. If you followed this example it will be called example, simply type ./example to run it, it should print Hello World! now.

What we have done so far probably seems like a lot of work to compile a single source file, and it is. But the nice thing is that it does not only work for a single source file, every source file in src and every header in include will be automatically compiled with this CMake procedure, try it out! Define some headers and more source files and see what happens.

Linking Against Existing Projects

Where CMake really shines is linking to existing code and libraries. CMake can find the location of libraries for us and link against them almost completely automatically.

Consider the following program, that doesn’t do anything useful, but depends on Eigen, Boost and OpenMP. Copy paste it into the main.cpp file.

#include <iostream>
#include <Eigen/Dense> // Needed to acces Eigen3's vectors and matrices
#include <boost/format.hpp> // Needed to acces Boosts formatter
#include <omp.h> // Needed for the parallel tools from OpenMP

int main(){
  /*****************/
  /* USING EIGEN 3 */
  /*****************/
  // create a 3-vector (1,2,3)
  Eigen::Vector3d vec;
  vec << 1,2,3;
  std::cout << "Print the vector" << std::endl;
  std::cout << vec << std::endl;
  // create a 3x3 matrix
  Eigen::Matrix3d mat;
  mat << 1,2,3,4,5,6,7,8,9;
  std::cout << "Print the matrix" << std::endl;
  std::cout << mat << std::endl;
  // Compute the product
  std::cout << "This is their product" << std::endl;
  std::cout << mat * vec << std::endl;

  /*****************/
  /* USING BOOST   */
  /*****************/
  std::cout << boost::format("This will be a formatted number: %1.4f") %
                 1.67329587;

  /**************************/
  /* An OpenMP EXAMPLE      */
  /**************************/
  const int size = 25600;
  double sinTable[size];

  // We use half the available threads, just to see what it does in the task
  // manager or htop. For a heavy application you would want to use as many as possible
  int maxThreads = omp_get_max_threads();
  int nrOfThreadsToUse = maxThreads / 2;
  #pragma omp parallel for num_threads(nrOfThreadsToUse)
  for (int n = 0; n < size; ++n) {
    // What we do here is useless, but it takes some time
    // so you can inspect windows task manager (windows), debug panel or htop(Linux)
    // to see that the for loop is run over multiple cores at the same time.
    sinTable[n] = std::sin(2 * 3.14 * n / size);
    for (int n2 = 0; n2 < size; ++n2) {
      sinTable[n] += std::sin(2 * 3.14 * n / size) + n2;
    }
  }
}

To let CMake know that we want to compile this file using Eigen, Boost and OpenMP we need to adapt the CMakeLists.txt file. What follows is the updated CMake file with comments explaining what is going on.

cmake_minimum_required (VERSION 3.8)
project ("example")

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# The nice thing about CMake is that it can find dependecies for us and handle
# the linking and include files, to find a package we do the following
find_package(Eigen3 3.3 REQUIRED)
find_package(Boost REQUIRED)
find_package(OpenMP)

# Besides our own include directory also Boost has one, since we used CMake to
# find the package for us, the include folder is stored in ${Boost_INCLUDE_DIRS}
# so our include directories are our own include directory and the Boost ones.
include_directories(include ${Boost_INCLUDE_DIRS})

file(GLOB SOURCES "src/*.cpp")
add_executable (example ${SOURCES})

# Since we use external libraries, we need to link our executable to them.
# Here we link agains Eigen, Boost and OpenMP.
# The PUBLIC keyword is used to specify the type of interface, in most cases PUBLIC
# will be fine.
target_link_libraries(example PUBLIC Eigen3::Eigen ${Boost_LIBRARIES} OpenMP::OpenMP_CXX)

install(TARGETS example DESTINATION bin)

If you now rebuild the project with CMake with the commands explained above, the project will be automatically compiled and linked with the correct libraries.

Tips and Tricks

When things go wrong

Sometimes things go wrong with CMake, to reconfigure you project completely from scratch, you can simply delete the build folder and start over. For larger projects that is not the smartest idea, because rebuilding everything might take quite some time. Instead you can navigate to the build folder and only delete the CMakeCache.txt file. If you then reconfigure CMake, it will reconfigure from scratch, but everything that is already build will not be rebuild.

Speeding Things Up Even More

We have used CMake now to take care of the whole build process (compiling, linking etc.) and once the CMakeLists.txt file is created we don’t have to think about it anymore. For the linux power users we can take everything one step further, by scripting the CMake commands.

Consider the following example script

#!/bin/bash
cmake -Bbuild_dir -DCMAKE_INSTALL_PREFIX=./ . || exit 1
cmake --build build_dir --parallel 6 --target install || exit 1 
bin/example

If we save this script in a file called run in the main directory and make it executable with chmod +x, we can simply type ./run to configure, build, install and run our project and we don’t have to worry about anything related to the build process anymore. Note that we need the || exit 1 to exit the shell if the configuration or build fails.