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.

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?

  1. Global again. set(CMAKE_CXX_STANDARD 20) applies to every target, including third-party subdirectories that wanted C++17.
  2. 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.
  3. It travels. A PUBLIC or INTERFACE requirement flows to consumers. Your header using std::span demands 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_20 is 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_XX means “at least this standard”. Ask for cxx_std_20 and 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_STANDARD plus CMAKE_CXX_STANDARD_REQUIRED ON plus CMAKE_CXX_EXTENSIONS OFF is 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, the gnu++ modes, are a separate setting. By default cxx_std_20 does not turn them off. If you need strict -std=c++20, set the CXX_EXTENSIONS property to OFF.)

Read the docs