foonathan::blog()

Thoughts from a C++ library developer.

Tutorial: Managing Compiler Warnings with CMake

Warnings are important, especially in C++.

C++ compilers are forced to accept a lot of stupid code, like functions without return, use of uninitialized warnings, etc. But they can at least issue a warning if you do such things.

But how do you manage the very compiler-specific flags in CMake?

How do you prevent your header files from leaking warnings into other projects?

My Previous Approach

Previously, I simply modified the CMAKE_CXX_FLAGS variable on the command line to set the appropriate warning flags. So on CI, for example, I invoked CMake with:

cmake -DCMAKE_CXX_FLAGS="-Werror -Wall -Wextra …"

That way the compiler will always have the warning flags enabled.

While this approach definitely works, it has a couple of problems:

  1. You have to remember to manually update CMAKE_CXX_FLAGS on CI and on every locale development machine. I occasionally forgot to do that, implemented a feature, pushed it to CI. There compilation failed due to warnings, which was annoying.

  2. The warnings are used to compile everything with warnings enabled. This is problematic when you use add_subdirectory() to compile some external dependencies which do not compile without warnings. You either have to remove -Werror or manually disable warnings on the external target somehow.

  3. It decouples the warning options from your version control system and build files. I think this is problematic, because your code is designed with a certain warning level in mind. This should also be reflected by the build files.

  4. It doesn’t feel particularly clean.

So with my latest project, foonathan/lex, I looked for a better solution.

Enabling Warnings by Modifying Target Properties

If -DCMAKE_CXX_FLAGS="…" is annoying, why not move it into the CMakeLists.txt?

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} …")

Don’t do this!

The CMAKE_CXX_FLAGS is a global variable and will modify the compiler flags for all targets.

Instead do this:

add_library(my_library …) 
target_include_directories(my_library PUBLIC include/)
target_link_libraries(my_library PUBLIC other_library)
target_compile_options(my_library PRIVATE "-Werror -Wall -Wextra")

When creating a library you specify include directories and link to other libraries. With target_compile_options() you can also specify compiler flags for your target. You can use that to specify warnings as well. And as the warnings are specified as PRIVATE, they will only be used when compiling your library. Targets linking to it will not get the warnings enabled. On the contrast, targets linking to it will get the include directories and other libraries as they are PUBLIC.

Tip: Use target_coompile_options(my_target PRIVATE "…") to enable warnings on your target.

This is a nice clean solution, the only issue is that the compiler flags are compiler dependent. The above warnings will work on GCC and clang, but not MSVC.

Before you start doing if(), take a look at generator expressions:

target_compile_options(my_library PRIVATE
                           # clang/GCC warnings
                           $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:GNU>>:
                                "-Wall">
                           # MSVC warnings
                           $<$<CXX_COMPILER_ID:MSVC>:
                                "/W4">)

This code will enable -Wall for GCC and clang and /W4 for MSVC.

Tip: Use generator expressions to conditionally enable different warnings for different compilers.

Preventing Warnings in Header Files

So with that you have warnings automatically enabled when compiling your library, and will hopefully fix all of them. But what if you are used by another project that has more warnings?

For example, I compile with -Wconversion but my dependencies don’t. So the header files have a couple of instances where the warning is issued, which is annoying.

There is not much I can do besides pull-requests to fix those warnings or locally disabling them, but as a library writer you can prevent the issue for projects with you as a dependency.

The trick is to use target_include_directories(my_library SYSTEM PUBLIC include/). The SYSTEM turns the include directory into a system include directory. Compilers will not issue warnings from header files originating from there.

So an external project linking my_library will not get any warnings from the header files of my library. But the source files of my library will not get warnings either!

When including the header files in my source files, I want warnings. But when including them from other source files, I don’t want them. So you might try something like this:

add_library(my_library …) 
target_include_directories(my_library PRIVATE include/
                                      SYSTEM PUBLIC include/)

Note that PRIVATE is first, then SYSTEM PUBLIC. Once you’ve used SYSTEM everything following it will be SYSTEM as well, so put it last.

You will privately add the include/ without SYSTEM, but publicly with. Sadly, this doesn’t work.

But you’re almost there:

add_library(my_library …) 
target_include_directories(my_library PRIVATE include/
                                      SYSTEM INTERFACE include/)

You have to use INTERFACE instead of PUBLIC. The interface properties are only given to external targets linking to your target, and never used when compiling the target itself. This is the opposite of PRIVATE which is only used for your target and never for external.

The reason it didn’t work with PUBLIC was because public properties are both PRIVATE and INTERFACE.

Guideline: Specify include directories for libraries twice. Once with PRIVATE and once with SYSTEM INTERFACE. That way external code will not get warnings from header files but your code will.

Handling Header-Only Libraries

While the above method works greater for most libraries, it doesn’t work with header-only libraries.

If you’re a good citizen you’ve created an interface library target:

add_library(my_library INTERFACE)
target_sources(my_library INTERFACE …)
target_include_directories(my_library SYSTEM INTERFACE include/)

That way users of the library can just use target_link_libraries() and will get the proper include paths automatically.

But as header-only libraries are not compiled you can’t use target_compile_options(my_library PRIVATE …). An interface library can only have INTERFACE targets.

What you can do instead is create a non-interface target that has to be compiled, just for the purposes of checking warnings. And you hopefully have one such target anyway, the tests!

add_executable(my_library_test …)
target_link_libraries(my_library_test PUBLIC my_library)
target_compile_options(my_library_test PRIVATE "…")

Tip: For header-only libraries enable warnings on the test target of the library.

But there’s one issue: As the test target links to the header-only target, it will get the SYSTEM include so you won’t actually get any warnings!

Adding the include directory again but without SYSTEM doesn’t seem to work reliably, so I don’t know any other solution besides duplicating the configuration of the my_library target for my_library_test as well, instead of linking to it.

If you know anything, please let me know.

Which Warnings Should I Enable?

Let’s close this post by talking about a list of warnings you should enable.

Disclaimer: This is very subjective.

For GCC/clang I usually have the following set of warnings:

  • -Werror: Treat warnings as errors. I like this one because it forces me to fix warnings. Also it makes it impossible to miss a warning. Without that flag a warning is generated when compiling, but you might miss it. Later compilation doesn’t touch that file again, so the warnings is not emitted again.

  • -pedantic-errors: This enables strict standard conformance, basically. Note that this is not equivalent to -Werror -pedantic, because why would it?

  • -Wall: A better name would be -Wcommon. It enables common warnings like use of uninitialized variables.

  • -Wextra: Some more common warnings not enabled by -Wall.

  • -Wconversion: Enables warnings about conversions that might change the value like float to int.

  • -Wsign-conversion: Enables warnings about conversions between signed and unsigned. Somewhat annoying, but still useful. Note that it is not covered by -Wconversion in C++ mode (for some reason).

Of course, there are more warnings not enabled by those ones. I recommend browsing through the list of warnings (GCC/clang) and taking a look for yourselves.

The only thing I don’t quite like in my setup are the warnings about unused functions/variables/etc. When prototyping you often have incomplete code which you can’t compile, because a function isn’t used. But they did prevent a couple of bugs, so I’ll keep them enabled.

For MSVC I use /WX /W4. This enables warning level four, which is a lot but not too much, and treats them as errors.

Conclusion

Use target_compile_options() and generator expressions to enable the warnings for your library target, but use PRIVATE to prevent them from enabling warnings in projects linking to your target. Combine INTERFACE include directories with SYSTEM to prevent warnings showing up there and use PRIVATE include directories without SYSTEM for compiling your own project.

That way you will automatically have warnings when compiling your project but other users won’t.

This post was made possible by my Patreon supporters. If you'd like to support me as well, please head over to my Patreon and do so! One dollar per month can make all the difference.