Contents
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:
- Hello CMake!
- Another Target and the Project
- CMake Project Structure
- 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.
Permalink
Hi Arne, thanks for your nice article!
I meet a problem:
cxx_std_11
intarget_compile_features
forbids testmain.cpp(, which #include “catch.hpp”) also usescxx_std_11
, however,testmain.cpp
also includeshello.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.
Permalink
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 linetarget_compile_features(hello_lib PUBLIC cxx_std_14)
after the target’s definition:PUBLIC
, instead ofINTERFACE
because not only the targets that link againsthello_lib
have to compile with C++14, buthello_lib
itself as well.Permalink
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.
Permalink
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 😉