Inquiring C++ standard from build target for analyzer config - possible regression, bug or missing documentation?

Context

We have some convenience functions to configure the code analyzers (cppcheck and clang-tidy) via the builtin cmake mechanisms (Properties CXX_CLANG_TIDY and CXX_CPPCHECK). As we use this in multiple projects and with multiple compilers and operating systems these functions get a cmake target and a “level” and should the automatically configure the analyzers. For the respective tool command line we need to specify the C+±Standard (e.g. --std=c++17) as we use different Standards in different projects. For that we use get_target_property() on the target, whicht got its c++ standard via target_compile_fatures()

Problem

This used to work flawlessly with ubuntu 20.04 (cmake 3.16), but after upgrading to Ubuntu 22.04 (cmake 3.22) that mechanism is broken. target_compile_features() does not seem to set the property CXX_STANDARD any more.

Questions

  • Is this a bug or a regression?
  • If not, this intended behaviour - so what’s the rationale behind that? Should it be documented somewhere?

Minimal example to reproduce

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)

project(minimal_example
        LANGUAGES CXX)

add_executable(minexample)

set(CMAKE_MAKE_PROGRAM ninja)

target_compile_features(minexample
    PRIVATE
        cxx_std_17
)

get_target_property(TGT_CXX_STANDARD minexample CXX_STANDARD)

message(STATUS "Got CXX_STANDARD=${TGT_CXX_STANDARD}")

Output on Ubuntu 20.04:

[proc] Executing command: /usr/bin/cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/gcc -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/g++ -S/workspaces/miDAS -B/workspaces/miDAS/build -G Ninja
[cmake] Not searching for unused variables given on the command line.
[cmake] -- The CXX compiler identification is GNU 9.4.0
[cmake] -- Check for working CXX compiler: /usr/bin/g++
[cmake] -- Check for working CXX compiler: /usr/bin/g++ -- works
[cmake] -- Detecting CXX compiler ABI info
[cmake] -- Detecting CXX compiler ABI info - done
[cmake] -- Detecting CXX compile features
[cmake] -- Detecting CXX compile features - done
[cmake] -- Got CXX_STANDARD=17
[cmake] -- Configuring done

Output on Ubuntu 22.04:

[proc] Executing command: /usr/bin/cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/gcc -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/g++ -S/workspaces/miDAS -B/workspaces/miDAS/build -G Ninja
[cmake] Not searching for unused variables given on the command line.
[cmake] -- The CXX compiler identification is GNU 11.3.0
[cmake] -- Detecting CXX compiler ABI info
[cmake] -- Detecting CXX compiler ABI info - done
[cmake] -- Check for working CXX compiler: /usr/bin/g++ - skipped
[cmake] -- Detecting CXX compile features
[cmake] -- Detecting CXX compile features - done
[cmake] -- Got CXX_STANDARD=TGT_CXX_STANDARD-NOTFOUND
[cmake] -- Configuring done

This may have appeared to do what you want in the past, but it isn’t robust. There are a number of things that can affect the language standard that ultimately gets used in the compiler command for a target:

  • The target’s own compile feature settings.
  • Transitive compile features of targets it links to.
  • The target’s own <LANG>_STANDARD property (<LANG>_STANDARD_REQUIRED is also relevant).

CMake looks at all of those and works out the highest language standard required across the whole set. It then ensures that the language standard used during compilation is at least that. There’s nothing to guarantee that CMake won’t select something higher than any of those. The reason for this relates to some toolchains not offering older language standards, or having a higher standard with its default settings. There was some work related to this across the CMake versions you’ve mentioned which made things more rigorous and more accurate, but I don’t know the exact details. @tambre should be able to provide more details if required.

To my knowledge, it never did that before, but maybe @tambre can confirm. The two mechanisms are separate, but as explained above, CMake enforces the higher constraint of these. I can’t explain how you got the result with Ubuntu 20.04 though, that is suspicious.

There’s a FIXME in cmStandardLevelResolver.cxx that seems related. Essentially target_compile_features() effects are observable in some cases during the configuration stage. They aren’t intended to be and we need a policy eventually to fix this.

I teste various versions between 3.16 and 3.26-rc5 on Windows with MSVC and I’m unable to reproduce the issue. It functions like “Ubuntu 20.04” for all of them.

Thank you very much @craig.scott and @tambre for sharing these insights!

I’m happy to learn that this was not the best and most robust approach - which for me immediately sparks the question: what would be the right and robust approach? Is there a way to interrogate CMake, which language standard it selected for a given target while configuring it?

You should be able to use the $<COMPILE_FEATURES:features> to get the final value during the generation phase when. Should presumably work with add_custom_command()/add_custom_target().

$<TARGET_PROPERTY:prop> might work as well.

Unfortunately this is “to late” - the analyzers are configured via calls to set_target_properties(tgt PROPERTIES CXX_CPPCHECK cmdline) where I can not use generator expressions. But I understand that information might not be available at the time I call that function.

Any help to break this circular reasoning is welcome…

It seems the fix would be to support generator expressions for <LANG>_CPPCHECK like we do already for LINK_LIBRARIES, INCLUDE_DIRECTORIES, COMPILE_DEFINITIONS. Seems like it should be just a few lines of code.

@craig.scott Agreed on this approach?

If that is possible that should be supported for CXX_CLANG_TIDY as well (and maybe there are even more tools I’m not aware of…)

Adding support for generator expressions to <LANG>_CPPCHECK seems reasonable. You could mount a similar argument for <LANG>_CPPLINT, <LANG>_INCLUDE_WHAT_YOU_USE and <LANG>_CLANG_TIDY as well. CMake 3.25 did that for <LANG>_COMPILER_LAUNCHER.