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
- An IDE or text editor
- CMake
- Boost
- 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.