The goal
When building C++ code with CMake, it is very common to want to set some pre-processor defines in the CMake code.
For instance, we might want to set the project's version number in a single place, in CMake code like this:
This sets the CMake variable PROJECT_VERSION
to 1.5, which we can then use to pass -DMYAPP_VERSION_STRING=1.5
to the C++ compiler. The about dialog of the application can then use this to show the application version number, like this:
Similarly, we might have a boolean CMake option like START_MAXIMIZED
, which the user compiling the software can set to ON or OFF:
If it's ON, you would pass -DSTART_MAXIMIZED
, otherwise nothing. The C++ code will then use #ifdef
. (We'll see that there's a better way.)
The common (but suboptimal) solution
A solution that many people use for this is the CMake function add_definitions
. It would look like this:
Technically, this works but there are a number of issues.
First, add_definitions
is deprecated since CMake 3.12 and add_compile_definitions
should be used instead, which allows to remove the leading -D
.
More importantly, there's a major downside to this approach: changing the project version or the value of the boolean option will force CMake to rebuild every single .cpp file used in targets defined below these lines (including in subdirectories). This is because add_definitions
and add_compile_definitions
ask to pass -D
to all cpp files, instead of only those that need it. CMake doesn't know which ones need it, so it has to rebuild everything. On large real-world projects, this could take something like one hour, which is a major waste of time.
A first improvement we can do is to at least set the defines to all files in a single target (executable or library) instead of "all targets defined from now on". This can be done like this:
We have narrowed the rebuilding effect a little bit, but are still rebuilding all cpp files in myapp, which could still take a long time.
The recommended solution
There is a proper way to do this, such that only the files that use these defines will be rebuilt; we simply have to ask CMake to generate a header with #define
in it and include that header in the few cpp files that need it. Then, only those will be rebuilt when the generated header changes. This is very easy to do:
We have to write the input file, myapp_config.h.in
, and CMake will generate the output file, myapp_config.h
, after expanding the values of CMake variables. Our input file would look like this:
A good thing about generated headers is that you can read them if you want to make sure they contain the right settings. For instance, myapp_config.h
in your build directory might look like this:
For larger use cases, we can even make this more modular by moving the version number to another input file, say myapp_version.h.in
, so that upgrading the version doesn't rebuild the file with the showMaximized()
code and changing the boolean option doesn't rebuild the about dialog.
If you try this and you hit a "file not found" error about the generated header, that's because the build directory (where headers get generated) is missing in the include path. You can solve this by adding set(CMAKE_INCLUDE_CURRENT_DIR TRUE)
near the top of your CMakeLists.txt
file. This is part of the CMake settings that I recommend should always be set; you can make it part of your new project template and never have to think about it again.
There's just one thing left to explain: what's this #cmakedefine01
thing?
If your C++ code uses #ifdef
, you want to use #cmakedefine
, which either sets or doesn't set the define. But there's a major downside of doing that -- if you forget to include myapp_config.h
, you won't get a compile error; it will just always go to the #else
code path.
We want a solution that gives an error if the #include
is missing. The generated header should set the define to either 0 or 1 (but always set it), and the C++ code should use #if
. Then, you get a warning if the define hasn't been set and, because people tend to ignore warnings, I recommend that you upgrade it to an error by adding the compiler flag -Werror=undef
, with gcc or clang. Let me know if you are aware of an equivalent flag for MSVC.
And these are all the pieces we need. Never use add_definitions
or add_compile_definitions
again for things that are only used by a handful of files. Use configure_file
instead, and include the generated header. You'll save a lot of time compared to recompiling files unnecessarily.
I hope this tip was useful.
For more content on CMake, we curated a collection of resources about CMake with or without Qt. Check out the videos.
Trusted software excellence across embedded and desktop platforms
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
9 Comments
13 - Nov - 2024
Renaud G.
I stopped using define in code for configuration. I generate a file using configuration_file from cmake but I set a namespace config where I put many values in constexpr: This is my config.h.in:
13 - Nov - 2024
David
What about setting COMPILE_DEFINITIONS property on source files? It avoids have to rebuild everything problem but you also do not need to create, configure and include a header?
14 - Nov - 2024
David Faure
Interesting idea. But in my opinion it falls into the "too much magic" category, because it's hard to see where the value comes from or what it's set to, and it's easy to forget to set it... More precisely:
With a generated header you can just do "Go to definition" in your IDE and jump to where the value is set; you can't do that if it's a compile definition from CMake.
If you move code around, will you remember to edit the CMakeLists.txt accordingly? It seems very separate from the actual code, compared to moving a #include together with the code that needs it.
15 - Nov - 2024
David
Yeah I fully agree
14 - Nov - 2024
Giuseppe D'Angelo
The equivalent of -Wundef would be C4668, https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4668?view=msvc-170
So adding something like /we4668 would treat this as an error. Note that you need to disable warnings in system headers -- Windows SDK headers otherwise trigger this. https://gcc.godbolt.org/z/P91GcY9PT
14 - Nov - 2024
Tobias
One more thing that I use regularly to speed up re-compiles is having extern const char MY_GIT_DESCRIBE[]; in config.h.in and in config.cpp.in I have const char MY_GIT_DESCRIBE[] = "@GIT_DESCRIBE@"; That way only the generated
config.cpp
changes each commit and needs to be recompiled, the rest is just linking. Depending on what parts of the code includeconfig.h
that can save a lot of time.16 - Nov - 2024
Jens Alfke
I don’t believe that’s true. I’ve often used #if with a symbol that’s possibly undefined; there's no warning, and it evaluates to 0.
I just tested it in Clang 16 and got no warning, and I’ve got nearly every warning enabled. (Literally: I use -Weverything and then disable individual warnings I don’t want.)
17 - Nov - 2024
David Faure
Do you disable -Wundef then? Because in my testing, clang 16's -Weverything does include -Wundef, and does warn on #if undefined_symbol. See https://godbolt.org/z/8qnqx7PcE
In any case my recommendation is to add -Werror=undef
4 - Dec - 2024
David Faure
Here's a new reason against add_definitions: it doesn't work well with double-quotes and spaces in the value. Just had an issue with moc (from Qt 6.8.1) in a customer project. Porting to configure_file and #include fixed it, there we have much more control over quoting.