Skip to content

Setting C++ Defines with CMake Why you should use configure_file instead of add_definitions

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:

  project(MyApp VERSION 1.5)

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:

  const QString aboutString = QStringLiteral("My App version: %1").arg(MYAPP_VERSION_STRING);
  QMessageBox::information(this, "My App", aboutString);

Similarly, we might have a boolean CMake option like START_MAXIMIZED, which the user compiling the software can set to ON or OFF:

  option(START_MAXIMIZED "Show the mainwindow maximized" 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.)

  #ifdef START_MAXIMIZED
      w.showMaximized();
  #else
      w.show();
  #endif

The common (but suboptimal) solution

A solution that many people use for this is the CMake function add_definitions. It would look like this:

  add_definitions(-DMYAPP_VERSION_STRING="${PROJECT_VERSION}")
  if (START_MAXIMIZED)
     add_definitions(-DSTART_MAXIMIZED)
  endif()

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:

  target_compile_definitions(myapp PRIVATE MYAPP_VERSION_STRING="${PROJECT_VERSION}")
  if(START_MAXIMIZED)
     target_compile_definitions(myapp PRIVATE START_MAXIMIZED)
  endif()

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:

  configure_file(myapp_config.h.in myapp_config.h)

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:

  #define MYAPP_VERSION_STRING "${PROJECT_VERSION}"
  #cmakedefine01 START_MAXIMIZED

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:

  #define MYAPP_VERSION_STRING "1.5"
  #define START_MAXIMIZED 1

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.

  if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options(myapp PRIVATE -Werror=undef)
  endif()

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.

To get into this topic even in more detail, watch this complimentary video on YouTube:

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

Categories: C++ / CMake / KDAB Blogs / KDAB on Qt / Technical

9 thoughts on “Setting C++ Defines with CMake”

  1. 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:
    “`
    namespace config
    {
    constexpr int major{@PROJECT_VERSION_MAJOR@};
    constexpr int minor{@PROJECT_VERSION_MINOR@};
    constexpr int patch{@PROJECT_VERSION_PATCH@};

    constexpr auto version{“@PROJECT_VERSION@”};
    }
    “`

  2. 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?

    1. 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.

  3. 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 include `config.h` that can save a lot of time.

  4. > the C++ code should use #if. Then, you get a warning if the define hasn’t been set

    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.)

  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *