Lesson 12 · Targets: the core model
Choosing the C++ standard
Set C++20 or C++23 per target with target_compile_features. Why the docs prefer it over the global CMAKE_CXX_STANDARD variable.
Every C++ project must choose a language standard. It is one of the few settings where the docs have a clear “do this, not that”, and it fits the target model perfectly.
The recommended way
target_compile_features(app PRIVATE cxx_std_20)
That is it. CMake adds whatever flag your compiler needs, -std=c++20, /std:c++20, automatically, and errors out if your compiler cannot do C++20.
Why not CMAKE_CXX_STANDARD 20
You will see this everywhere, including in old official tutorials:
Don't do this# Works, but global. Affects every target below it. set(CMAKE_CXX_STANDARD 20)
Do this# Per target. target_compile_features(app PRIVATE cxx_std_20)
The official tutorial (Step 4, exercise 1) is explicit. It cautions against globally setting CMAKE_<LANG>_STANDARD, and recommends target_compile_features() instead. Why?
- Global again.
set(CMAKE_CXX_STANDARD 20)applies to every target, including third-party subdirectories that wanted C++17. - It hides incompatibilities. With the global variable, a library that needs C++17 quietly gets C++20 and nobody notices, until it breaks. A per-target requirement says exactly what each piece needs.
- It travels. A
PUBLICorINTERFACErequirement flows to consumers. Your header usingstd::spandemands C++20 from whoever includes it, at build time, with a clear error.
The three keywords, applied to standards
# My headers use C++20, so consumers must compile with C++20 too.
target_compile_features(mylib PUBLIC cxx_std_20)
# Only my .cpp files use C++20. Nobody else cares.
target_compile_features(mylib PRIVATE cxx_std_20)
# Header-only library. Nothing to compile, consumers inherit.
target_compile_features(myheaderlib INTERFACE cxx_std_20)
Why
PUBLIC cxx_std_20is how a library enforces its contract. If a consumer compiles the header with C++17, that is a hard error from CMake, not a confusing template explosion from the compiler. The requirement lives on the target that owns it. Lesson 7 again.
What is available
The meta-features for every standard since C++11 are cxx_std_11, cxx_std_14, cxx_std_17, cxx_std_20, cxx_std_23, cxx_std_26.
target_compile_features(app PRIVATE cxx_std_23) # C++23
Note
cxx_std_XXmeans “at least this standard”. Ask forcxx_std_20and you get C++20 or newer, never C++17. CMake picks the newest the compiler supports if it has to, but it never goes below what you asked for.
When CMAKE_CXX_STANDARD is acceptable
There is an acceptable fallback. In a tiny project where you own every target, set(CMAKE_CXX_STANDARD 20) at the top works fine. Even then, prefer the target form. It costs nothing and stays correct when the project grows.
Pitfall
CMAKE_CXX_STANDARDplusCMAKE_CXX_STANDARD_REQUIRED ONplusCMAKE_CXX_EXTENSIONS OFFis the old global trio from blog posts of 2015.target_compile_features(mylib PUBLIC cxx_std_20)replaces all three in one line. (Note that extensions, thegnu++modes, are a separate setting. By defaultcxx_std_20does not turn them off. If you need strict-std=c++20, set theCXX_EXTENSIONSproperty toOFF.)
Read the docs