CMake – Properties and Options

Today I’ll continue the little CMake tutorial series. We’ll add a few options and a bit of fine-tuning to the compilation of our example project.

This post is part of a series about CMake:

  1. Hello CMake!
  2. Another Target and the Project
  3. CMake Project Structure
  4. Properties and Options

Specify required compiler features

If you still have access to some old compiler, you may have noticed that our little project does not compile. To be honest, I have not tested it myself, but Catch documents that it needs a bunch of C++11 features. So we can expect the compilation to fail with a bunch of errors on compilers that do not support those features.

With CMake, we have the possibility to require compiler features for our targets. Since we currently do not use C++11 features anywhere except those required by Catch, we should add those requirements to Catch’s CMakeLists.txt, using the target_compile_features command:

project (Catch)

# Header only library, therefore INTERFACE
add_library(catch INTERFACE)

# INTERFACE targets only have INTERFACE properties
target_include_directories(catch INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_features(catch INTERFACE cxx_std_11)

As with the other commands, we use INTERFACE, because the feature cxx_std_11 is required for the compilation of anything that uses Catch. The obvious features are those for the language standards C++98 through C++20, the latter having been added shortly before I wrote this with CMake 3.12. There’s a whole bunch of more detailed single features that may enable a few more compilers that do not support the full standard yet. For example, Catch defines its own CMakeLists.txt, and the target_compile_features command it uses looks like this:

target_compile_features(Catch2
  INTERFACE
    cxx_alignas
    cxx_alignof
    cxx_attributes
    cxx_auto_type
    cxx_constexpr
    cxx_defaulted_functions
    cxx_deleted_functions
    cxx_final
    cxx_lambdas
    cxx_noexcept
    cxx_override
    cxx_range_for
    cxx_rvalue_references
    cxx_static_assert
    cxx_strong_enums
    cxx_trailing_return_types
    cxx_unicode_literals
    cxx_user_literals
    cxx_variadic_macros
)

Define some macro values

While macros are often frowned upon, the reality is that we still sometimes use them to configure our applications at compile time. Obviously, we do not use them in our little “hello CMake” example code, but Catch has a few of those options. Just as an example, let’s shorten the console width Catch uses to report errors. To do that, we have to define the macro constant CATCH_CONFIG_CONSOLE_WIDTH.

We do so by using the target_compile_definitions command. We’ll use it on the tests target because the configuration of Catch lies with the target that uses it, not with Catch itself:

project(hello_tests)

# The test program
add_executable(tests testmain.cpp)
target_link_libraries(tests PRIVATE hello_lib catch)
target_compile_definitions(tests PRIVATE CATCH_CONFIG_CONSOLE_WIDTH=60)

If we now build the project and run our tests, the width of the ==== separator will be reduced to 60:

$ test/tests.exe
===========================================================
All tests passed (1 assertion in 1 test case)

Enable compiler warnings

It is a good practice to enable a good measure of compiler warnings and even treat them all as errors. We can do this by adding some compiler options in CMake. Sadly, this is not possible in an entirely portable way, because those warnings are not standardized and the flags to enable them are different, depending on the compiler.

Conditionals in CMake

Luckily, CMake provides means for conditional execution like common programming languages. It also provides variables that determine the compiler. That way, we are able to add those flags differently per compiler. For the warnings, I’d like to distinguish between Visual Studio and other compilers, assuming that anything that is not Visual Studio will use Flags compatible to GCC.

if (MSVC)
  # do MSVC specific things
else()
  # do something else
endif()

Here, the MSVC variable is one of several variables provided by CMake which describe the system. It is set to true whenever we compile with MSVC or a compatible compiler.

Target vs. global

We could now go ahead and use the target_compile_options command to enable the warnings for each target individually. In general, the use of target-specific commands is encouraged in modern CMake, as it allows for better granularity instead of affecting all targets, including those we might add in the future.

There are, however, some cases where the configuration should affect all targets. Those include definitions and flags that influence the ABI – those have to be global, to ensure ABI compatibility. In the case of warnings, I’d also go for the global option. We’ll still be able to fine-tune the warnings of single targets if needed.

Therefore we’re going to use the add_compile_options command which affects the current directory and all directories below. It affects targets that are created after the command, so we’ll use it in the main CMakeLists.txt, before the inclusion of the target-specific subdirectories.

project(hello_cmake)

if (MSVC)
    # warning level 4 and all warnings as errors
    add_compile_options(/W4 /WX)
else()
    # lots of warnings and all warnings as errors
    add_compile_options(-Wall -Wextra -pedantic -Werror)
endif()

add_subdirectory(thirdparty/catch)
add_subdirectory(src)
add_subdirectory(test)

You can test that the warnings are turned on by adding the following line to any cpp file and trying to compile again:

void f(int i){} //warning and error due to unused i

Conclusion

We have now some of the tools we need to fine-tune the compilation of our project: compiler features, definitions and compile options. We also got a glimpse of conditional execution, for those cases where we have to use some nonportable bits and pieces.

As always, you can find the current status of the project on GitHub.

Previous Post
Next Post

4 Comments


  1. Hi Arne, thanks for your nice article!

    I meet a problem:

    cxx_std_11 in target_compile_features forbids testmain.cpp(, which #include “catch.hpp”) also uses cxx_std_11, however, testmain.cpp also includes hello.h, where I use c++14 feature(decay_t), so the source file “testmain.cpp” report error when build: “error: no template named ‘decay_t’ in namespace ‘std’; did you mean ‘decay’?”

    My workaround now is change target_compile_features(catch INTERFACE cxx_std_11) into `target_compile_features(catch INTERFACE cxx_std_17)

    But I’m not sure whether it is modern cmake, at least it works as expected.

    Any more proper solution? thanks in advance.

    Reply

    1. Hi chenli, thanks for the question. If hello.h is the file that uses a C++14 feature, then the project where it belongs to should specify that requirement. After all, it has nothing to do with Catch, so better leave the Catch target as it is.

      In this case, I’d edit src/CmakeLists.txt where the hello_lib target is defined, as that’s where hello.h belongs to. Add the line
      target_compile_features(hello_lib PUBLIC cxx_std_14) after the target’s definition: PUBLIC, instead of INTERFACE because not only the targets that link against hello_lib have to compile with C++14, but hello_lib itself as well.

      Reply

  2. Nice post.
    Obviously you were using it as a talking point to demonstrate how you’d deal with the situation in CMake – but if you really want to compile a Catch project with an older compiler you can grab the one from the Catch1.x branch. It doesn’t have all the latest features, but is still maintained to a certain level.

    Reply

    1. Hi Phil, thanks for the tip. You’re right, this is only an example for building the tutorial. I hope to come to the point where Catch’s own CMake files are used.

      After I learned myself how to do it properly, that is 😉

      Reply

Leave a Reply

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