Basic C++ setup with dependency management in CMake
When I started seriously learning C++ a couple of years ago, I struggled with a proper setup the most. Beginner tutorials often only use the command line, but lack in larger topics like build and dependency management. Where more advanced guides often assume greater knowledge with at least CMake already.
Coming from web development, dependency management was one of the
easy tasks, as simple as
npm install. Nevertheless,
building a web project got increasingly difficult over the years,
assuming the usage of tools like
and others. Of course there is still the option to not use any and
As I started writing websites with Internet Explorer 5, I kinda grew up with those tools, naturally, as they emerged over the years. In contrast, coming into the large C++ ecosystem with a lot of tools and practices feels like hitting a wall with high speed. Stepping back, starting without tools, and trying to understand every tool I add first, at least on a basic level; that was the plan. And the tool I wanted to understand first was CMake — little I knew what a ride that will be.
Before I start some words about platform support: I regularly work on a M1 Mac, but I also tested this setup with Windows 11 and Linux (Debian).
I've heard a lot of things about CMake, a lot of those things were bad. Nevertheless, CMake seems to be a very dominant tool in C++ development, and I definitely wanted to understand it, at least good enough to feel comfortable using it.
Searching the internet for resources I saw a lot of CMake code, solving the same problem in many ways. Initially I tried to combine what I found to reach my goals, but even with the little understanding of CMake I had this felt wrong. Not fully understanding the tool, I've got the same frustration probably many felt using CMake and its many versions. It was time to extend my CMake knowledge — I bought Professional CMake: A Practical Guide, that I've heard a lot of good things about. And the book was indeed an amazing source of knowledge. By the way, there is no affiliation, I just genuinely like the book.
From here on I will show how I set up my environment for C++ development with CMake. It may not be the best, but it proved valuable for me multiple times and is, with my limited experience, my favourite choice developing in C++.
The minimal project setup looks like this:
Going through it step-by-step; the first folder
build is to capture generated project files and the
actual application build. The folder is
excluded from source control. Inside are the
different build artefacts for debug and release builds, but can also
contain a different, or more granular, structure when building for
CMakeLists.txt looks like this:
The first line sets the minimum CMake version and can be any version for you, I like to use a rather recent version.
The next line includes a file inside the
UniversalAppleBuild.cmake is used to generate
universal builds for Apple Intel and Silicon. The contents are as
Next the project setup.
This will define the CMake project, give it a project name, a description and the language used; in this case C++20.
Standard project settings
The next include,
define some common settings.
It is located inside the
cmake folder that will hold
all CMake source files. In its basic form this files looks like
This will, if a Debug build is enabled, also define three compiler
APP_DEBUG to be used to enable debug build only code
APP_ENABLE_ASSERTS for assertions and
APP_PROFILE for performance profiling. All are prefixed
APP_, representative for a more unique prefix,
like your app name or a short form of it.
Now comes the code quality part, or at least some of it.
This looks like a hack, and it maybe is, but works like a charm. I
create an empty interface called
project_warnings and include another file,
CompilerWarnings.cmake, that will create a function
set_project_warnings that takes a
"library" name and attaches a set of compiler options to it
target_compile_options. I then can use this
interface and link it as a library to my app and libraries I want to
Here a shortened look inside the
CompilerWarnings.cmake file with some added comments
for clarification (the long version can be found
The last line inside the root
CMakeLists.txt file is to
include the source directory.
CMakeLists.txt inside the
contains two lines, one to include the library folder and one for
the app. This is dependent on the amount of applications and
libraries in your project.
Defining a library
Now let's have a look at the
The first line is, for convenience, the definition of the library name.
Then I include a file I add to every library as well as the app,
The clang-tidy options are defined in a
.clang-tidy file at the project root.
As many projects there are options, and this set of options is only mine. You can find a list of all checks for clang-tidy here.
Library files and build settings
Next some source files:
This will add needed source files to create a static library. I like splitting my code into smaller libraries, like "core", "ui", "rendering", etc. to create logical chunks, but also keep build times lower. This is due to only libraries with changed files need to be rebuilt, otherwise they will only be linked.
At last, setting the "include directory", building with
C++20, and linking the
project_warnings as mentioned
Defining an application
Alas, a look at the
src/app/CMakeLists.txt file and how
to define an executable.
Same as for the library, it defines a name for convenience.
Again, adding static analysers to the app.
Throw in all app source files.
And define the "include directory" and the C++ standard to build with.
Besides linking the
project_warnings as mentioned
earlier, I also link the libraries I want to use with the application by
name; in that case my example
And that's about it to define an application. This setup supports multiple applications sharing the same (or different) libraries. Making this a kind of mono repository.
How about the big topic dependencies? This was my greatest issue when starting C++, first using Git submodules, directly throwing source files into the project, failing at using Conan, and many other attempts. Though, finally I settled on using CMake's FetchContent.
The way I structure dependencies is by having a
vendor folder in the root of the project containing a
CMakeLists.txt and one folder per dependency with
to the project structure looks like this:
First, I include the
vendor folder inside the root
vendor/fmt/CMakeLists.txt file contains
a message for fetching the dependency, potential dependency settings
and the statement to make the dependency available to the project.
vendor/CMakeLists.txt file declares the actual
dependency and the version to fetch (and from where).
It includes the
FetchContent module from CMake, then
declares a dependency by giving it a name, what Git repository to
use and the Git tag (or alternatively a commit hash). At last, it
adds the dependency subdirectory.
With FetchContent_Declare not only Git repositories can be used, also SVN or any URL really containing an archive (e.g. a tar.gz file).
The benefit with FetchContent is that dependencies are fetched only once on configure time, not on every build as with other solutions.
Build and run
Before building the project, the CMake configuration step needs to be executed, looking like this for the debug setup (using Ninja to build the project):
Building the project:
Alas, running the generated executable (under Mac in that case):
A list of all options can be found here.
To format all the project source files, having clang-format installed, run:
As biased as I am, of course I use my own project
to define some common commands for the project. Adding a
litr.toml file in the project root to have quick
commands for building, running and formatting the project.
That's about it when it comes the base setup. I do actually have a little more, namely tests via doctest, but wanted to spare the setup here to keep it short. Nevertheless, this and a full example of the setup can be found on GitHub.
Template repository: https://github.com/MartinHelmut/cpp-base-template