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:

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.