Skip to content
C++
Library
since C++20
Basic

<version>

C++20 header that centralises all standard library feature-test macros for portable compile-time capability detection.

<version>since C++20

A header that defines the complete set of standard library feature-test macros (__cpp_lib_*), enabling compile-time detection of which C++ library features the current implementation supports—without pulling in any other declarations.

Overview

Before C++20, feature-test macros were only guaranteed to be defined when you included the header that provided the feature itself. Checking whether std::string::contains was available meant including <string> first—which might already pull in far more than you wanted, or might simply be absent on the target toolchain.

C++20 introduced <version> to decouple feature detection from feature inclusion. Every standard library feature-test macro that the implementation supports is defined here, covering more than 200 distinct capabilities spanning C++11 through C++23 and beyond. The header defines no types, no functions, and nothing in namespace std—it is purely preprocessor output.

Macro naming and values

Library feature macros follow the pattern __cpp_lib_<feature>. Each macro expands to a long integer literal of the form YYYYMML, encoding the year and month in which the feature was formally adopted by the standards committee. A value of 202011L means November 2020; 201907L means July 2019.

This encoding lets you distinguish between revisions of the same feature. A macro can be tested for existence (#ifdef) when you only need to know whether the feature is present at all, or tested against a threshold (#if __cpp_lib_ranges >= 202110L) when you need a specific revision that introduced a particular overload or behaviour change.

Language feature macros—__cpp_concepts, __cpp_structured_bindings, __cpp_constexpr, and so on—do not carry the _lib_ infix. They are defined by the compiler itself and are available without any include, though <version> also provides them.

Syntax

cpp
#include <version>

// Existence check — feature present at all
#ifdef __cpp_lib_three_way_comparison
#  include <compare>
#endif

// Version check — need a specific revision
#if defined(__cpp_lib_format) && __cpp_lib_format >= 202110L
#  include <format>
#endif

// __has_include as a softer fallback (available since C++17)
#if __has_include(<format>)
#  include <format>
#endif

__has_include (C++17) tests whether a header file exists in the implementation's search path; it does not guarantee the feature inside it conforms to any standard revision. Prefer __cpp_lib_* macros when you need semantic guarantees, and reserve __has_include for probing vendor extensions or headers whose presence is itself meaningful.

Examples

Portable fallback for a C++23 library addition

cpp
#include <version>
#include <string>

#ifdef __cpp_lib_string_contains   // C++23: value 202011L
void log_if_matches(const std::string& haystack, const std::string& needle) {
    if (haystack.contains(needle)) {
        // std::string::contains available
        report(haystack);
    }
}
#else
void log_if_matches(const std::string& haystack, const std::string& needle) {
    if (haystack.find(needle) != std::string::npos) {
        report(haystack);
    }
}
#endif

Both branches compile and produce identical runtime semantics. Only one is selected, based on what the translation environment actually supports.

Guarding range-based container operations

cpp
#include <version>
#include <deque>
#include <vector>

void append_all(std::deque<int>& dst, const std::vector<int>& src) {
#ifdef __cpp_lib_containers_ranges   // C++23: value 202202L
    dst.append_range(src);
#else
    dst.insert(dst.end(), src.begin(), src.end());
#endif
}

This pattern appears throughout codebases that target multiple toolchain versions simultaneously (embedded projects, open-source libraries with broad compiler support matrices).

Interrogating multiple macros at once

cpp
#include <version>
#include <cstdio>

void print_feature_support() {
    auto check = [](const char* name, long actual, long required) {
        std::printf("%-40s %s\n", name,
            actual >= required ? "YES" : "no");
    };

    check("std::format",
          __cpp_lib_format + 0L,        202110L);  // C++20 (revised)
    check("std::ranges::views",
          __cpp_lib_ranges + 0L,        201911L);  // C++20
    check("std::flat_map",
          __cpp_lib_flat_map + 0L,      202207L);  // C++23
    check("std::expected",
          __cpp_lib_expected + 0L,      202211L);  // C++23
    check("std::mdspan",
          __cpp_lib_mdspan + 0L,        202207L);  // C++23
}

Adding + 0L to an undefined macro would be a compilation error, so this only works if you have already verified the macros are defined, or if you wrap in #ifdef. A cleaner production approach keeps each check inside its own #ifdef block. The pattern above is useful for a feature-probe binary during configure-time inspection.

Discriminating feature revisions

<ranges> gained substantial extensions between C++20 and C++23. The macro value lets you target the exact revision:

cpp
#include <version>

#if __cpp_lib_ranges >= 202302L
// C++23 added std::ranges::zip, std::ranges::chunk, std::ranges::slide, etc.
#  include <ranges>
#  define HAS_RANGES_ZIP 1
#elif __cpp_lib_ranges >= 201911L
// C++20 baseline ranges: filter, transform, take, drop, ...
#  include <ranges>
#  define HAS_RANGES_ZIP 0
#else
#  define HAS_RANGES_ZIP 0
#endif

Comprehensive Macro Reference

The table below covers the most commonly tested __cpp_lib_* macros. The value column shows the macro value defined by an implementation that fully supports the feature; earlier or partial implementations may define a lower value or leave the macro undefined. All values are decimal long integer literals (suffix L is implicit in the standard's tables).

MacroValueStandardDescription
__cpp_lib_format202110LC++20std::format, std::format_to, std::formatted_size; C++23 adds range formatting and std::print
__cpp_lib_ranges201911LC++20Range concepts, views, and algorithms; C++23 revisions bring zip, chunk, slide (value 202302L)
__cpp_lib_expected202211LC++23std::expected<T,E> error-handling monad with monadic .and_then(), .or_else(), .transform()
__cpp_lib_flat_map202207LC++23std::flat_map and std::flat_set sorted-container adaptors with contiguous storage
__cpp_lib_mdspan202207LC++23std::mdspan non-owning multi-dimensional array view
__cpp_lib_optional202106LC++20std::optional; C++23 revision (value 202110L) adds monadic .and_then(), .or_else(), .transform()
__cpp_lib_span202002LC++20std::span non-owning contiguous-sequence view
__cpp_lib_concepts202002LC++20<concepts> header: std::same_as, std::derived_from, std::invocable, etc.
__cpp_lib_coroutines201902LC++20<coroutine>: std::coroutine_handle, std::suspend_always, std::suspend_never
__cpp_lib_jthread201911LC++20std::jthread auto-joining thread with cancellation via std::stop_token
__cpp_lib_atomic_wait201907LC++20std::atomic::wait(), notify_one(), notify_all() blocking primitives
__cpp_lib_constexpr_string201907LC++20std::string and std::vector fully usable in constexpr context
__cpp_lib_three_way_comparison201907LC++20<compare>: std::strong_ordering, std::partial_ordering, std::weak_ordering
__cpp_lib_string_contains202011LC++23std::string::contains() and std::string_view::contains()
__cpp_lib_containers_ranges202202LC++23Range-based overloads (insert_range, append_range, assign_range) on standard containers
__cpp_lib_stacktrace202011LC++23std::stacktrace and std::stacktrace_entry for capturing call-stack snapshots
__cpp_lib_generator202207LC++23std::generator<T> recursive coroutine range
__cpp_lib_execution201902LC++17Parallel algorithm execution policies (std::execution::par, std::execution::par_unseq)
__cpp_lib_print202207LC++23std::print and std::println in <print>; Unicode-aware, wraps std::format

Several macros appear at multiple values because the standard issued retroactive defect reports. Always check against the specific value you need, not merely for existence.

Language Feature Macros

Language feature macros share the __cpp_ prefix but omit the _lib_ infix. They are defined by the compiler (not the standard library), are available with no #include at all, and track the grammar and core-language rules rather than library types. <version> also exposes them, but they are pre-defined before the first header is processed.

The table below lists the most commonly tested language macros:

MacroTypical valueStandardWhat it guards
__cpp_concepts202002LC++20concept, requires expressions, requires clauses
__cpp_structured_bindings201606LC++17auto [x, y] = pair; decomposition syntax
__cpp_constexpr202110Lincrementalconstexpr expansions: functions (C++11), lambdas (C++17), virtual (C++20), etc.
__cpp_modules202002LC++20import, export module, module partitions
__cpp_coroutines201902LC++20co_await, co_yield, co_return keywords
__cpp_deduction_guides201703LC++17Class template argument deduction (CTAD)

Because language macros are compiler-defined, they reflect what the compiler's front-end supports independently of the installed standard library. A compiler can define __cpp_concepts while shipping a standard library that does not yet define __cpp_lib_concepts. Both must be checked when you need end-to-end feature coverage:

cpp
// C++20 — guard both compiler support and library support
#if defined(__cpp_concepts) && defined(__cpp_lib_concepts)
#  include <concepts>
template <typename T>
concept Printable = requires(T t) { std::cout << t; };
#endif

The separation is intentional: it lets library vendors ship updated headers without waiting for a new compiler release, and vice versa.

CMake Integration

CMake provides two complementary mechanisms for probing feature availability: high-level target_compile_features and low-level compile-probe checks. Use them together for robust cross-platform builds.

High-level standard requirement

cmake
# Require C++20 for the entire target — CMake sets -std=c++20 (or equivalent)
target_compile_features(mylib PUBLIC cxx_std_20)

This is the correct starting point for any C++20 project. It does not tell you which individual features the toolchain supports within that standard; it only sets the language dialect flag. Older compilers may accept -std=c++20 while silently omitting <format> or shipping it as experimental.

Feature-probe with CHECK_CXX_SOURCE_COMPILES

When you need to detect a specific library feature at configure time—for example to enable an optional fast path—use a compile probe:

cmake
include(CheckCXXSourceCompiles)

# Temporarily force C++20 mode for the probe
set(CMAKE_REQUIRED_FLAGS "-std=c++20")

check_cxx_source_compiles("
  #include <version>
  #if !defined(__cpp_lib_format) || __cpp_lib_format < 202110L
  #  error format not available
  #endif
  int main() { return 0; }
" HAVE_STD_FORMAT)

# Pass the result into the build as a preprocessor definition
if(HAVE_STD_FORMAT)
  target_compile_definitions(mylib PRIVATE MY_LIB_HAS_FORMAT=1)
endif()

The probe compiles a minimal translation unit that #errors out unless the macro meets the threshold. CMake records the result in HAVE_STD_FORMAT and caches it; subsequent configures skip the compilation unless you wipe the cache.

Combining both approaches

cmake
target_compile_features(mylib PUBLIC cxx_std_20)

include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS "-std=c++20")

foreach(feature IN ITEMS
    "__cpp_lib_format\;202110L\;FORMAT"
    "__cpp_lib_ranges\;201911L\;RANGES"
    "__cpp_lib_expected\;202211L\;EXPECTED"
)
  list(GET feature 0 macro)
  list(GET feature 1 value)
  list(GET feature 2 name)
  check_cxx_source_compiles("
    #include <version>
    #if !defined(${macro}) || ${macro} < ${value}L
    #  error missing
    #endif
    int main(){}" HAVE_${name})
  if(HAVE_${name})
    target_compile_definitions(mylib PRIVATE MY_LIB_HAS_${name}=1)
  endif()
endforeach()

This loop generates MY_LIB_HAS_FORMAT, MY_LIB_HAS_RANGES, and MY_LIB_HAS_EXPECTED compile definitions without repeating boilerplate. The granular approach catches toolchains that accept -std=c++20 but ship incomplete libraries—common on distributions that package GCC and libstdc++ separately.

Portable Compatibility Header Pattern

Scattering #ifdef __cpp_lib_* blocks throughout a large codebase is error-prone and hard to audit. The recommended practice is to centralize all detection in a single header—commonly named compat.hpp or feature_flags.hpp—that translates standard macros into project-local capability flags.

cpp
// include/mylib/compat.hpp
// Centralized feature detection — include this before any other mylib header.
// C++20

#pragma once

// --- 1. Pull in <version> only when it exists --------------------------------
#if defined(__cplusplus) && __cplusplus >= 202002L
#  include <version>
#endif

// --- 2. std::format ----------------------------------------------------------
#if defined(__cpp_lib_format) && __cpp_lib_format >= 202110L
#  define MY_LIB_HAS_FORMAT 1
#else
#  define MY_LIB_HAS_FORMAT 0
#endif

// --- 3. std::ranges ----------------------------------------------------------
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
#  define MY_LIB_HAS_RANGES 1
#else
#  define MY_LIB_HAS_RANGES 0
#endif

// --- 4. std::expected --------------------------------------------------------
#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202211L
#  define MY_LIB_HAS_EXPECTED 1
#else
#  define MY_LIB_HAS_EXPECTED 0
#endif

// --- 5. std::span ------------------------------------------------------------
#if defined(__cpp_lib_span) && __cpp_lib_span >= 202002L
#  define MY_LIB_HAS_SPAN 1
#else
#  define MY_LIB_HAS_SPAN 0
#endif

// --- 6. std::print / std::println (C++23) ------------------------------------
#if defined(__cpp_lib_print) && __cpp_lib_print >= 202207L
#  define MY_LIB_HAS_PRINT 1
#else
#  define MY_LIB_HAS_PRINT 0
#endif

// --- 7. Polyfill hooks -------------------------------------------------------
// Define MY_LIB_POLYFILL_* macros for features that have drop-in replacements.
// Downstream code uses MY_LIB_HAS_* exclusively; polyfill selection is hidden.

#if !MY_LIB_HAS_SPAN && __has_include(<gsl/span>)
// Fall back to GSL span if available and std::span is missing
#  include <gsl/span>
namespace mylib { template<typename T> using span = gsl::span<T>; }
#  define MY_LIB_POLYFILL_SPAN 1
#elif MY_LIB_HAS_SPAN
#  include <span>
namespace mylib { template<typename T> using span = std::span<T>; }
#  define MY_LIB_POLYFILL_SPAN 0
#else
#  error "mylib requires either std::span (C++20) or GSL span as a fallback."
#endif

Downstream headers then write only:

cpp
#include "mylib/compat.hpp"

#if MY_LIB_HAS_FORMAT
#  include <format>
#endif

// Use MY_LIB_HAS_FORMAT in logic — never check __cpp_lib_format directly again.

This pattern has three benefits: the detection logic lives in exactly one place; changing a polyfill strategy touches only compat.hpp; and CI failures are immediately localized to the compatibility layer rather than scattered across a hundred translation units.

Implementation Quirks

The standard specifies which macro to define and what value to use, but implementations sometimes deviate. Understanding per-compiler behaviour prevents subtle portability bugs.

GCC / libstdc++

GCC tends to define some __cpp_lib_* macros earlier than strict conformance warrants. A common pattern: the macro is defined at the correct value, but the implementation has known defect-report omissions or ABI-incompatible behaviour in edge cases. Always test the actual feature in CI, not only the macro value.

cpp
// C++20 — On some GCC 10/11 builds, __cpp_lib_format is defined but
// std::format is backed by a stub that throws std::bad_alloc on complex
// format specs. Probe with a runtime test during integration testing.
#if defined(__cpp_lib_format) && __cpp_lib_format >= 202110L
#  if defined(__GNUC__) && __GNUC__ < 13
#    define MY_LIB_FORMAT_NEEDS_RUNTIME_PROBE 1
#  endif
#  include <format>
#endif

MSVC / STL

MSVC sometimes ships a feature with the correct macro value even when the implementation is partial. In practice this is rare since Microsoft's STL team tracks the macro spec closely, but it does occur during preview releases. Conditional workarounds are best gated on _MSC_VER in addition to the standard macro:

cpp
// C++23 — MSVC preview builds may define __cpp_lib_expected
// before all monadic operations are complete.
#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202211L
#  if defined(_MSC_VER) && _MSC_VER < 1940
#    define MY_LIB_EXPECTED_MISSING_TRANSFORM 1
#  endif
#  include <expected>
#endif

Clang / libc++

Clang decouples the compiler (clang++) from the standard library (libc++). A __cpp_lib_* macro may be absent after #include <version> with one version of libc++ but defined with another, even if the compiler version is identical. The most robust probe combines __has_include, the macro check, and a minimum version:

cpp
// Maximum robustness pattern — works across all three major toolchains
#if __has_include(<format>)                          // header exists at all
#  include <version>
#  if defined(__cpp_lib_format) && __cpp_lib_format >= 202110L
#    include <format>
#    define MY_LIB_HAS_FORMAT 1
#  endif
#endif
#ifndef MY_LIB_HAS_FORMAT
#  define MY_LIB_HAS_FORMAT 0
#endif

The __has_include outer guard prevents the #include <version> from failing on toolchains that do not yet ship <version> at all (pre-C++20 environments where the -std flag was not passed). The macro version check then filters out headers that exist on disk but do not meet the required conformance level.

Difference from __has_cpp_attribute

The __has_cpp_attribute operator is a parallel but orthogonal facility: it probes for attribute support rather than library or language features. Like __cpp_lib_* macros, it returns an integer value encoding the year/month of standardization, and zero if the attribute is unknown.

cpp
// C++17 attribute — [[nodiscard]]
#if __has_cpp_attribute(nodiscard) >= 201907L
// The 201907L value corresponds to the C++20 revision that adds
// the optional reason string: [[nodiscard("use the return value")]]
#  define MY_NODISCARD [[nodiscard]]
#  define MY_NODISCARD_MSG(msg) [[nodiscard(msg)]]
#else
#  define MY_NODISCARD
#  define MY_NODISCARD_MSG(msg)
#endif

// C++20 attributes
#if __has_cpp_attribute(likely) >= 201803L
#  define MY_LIKELY   [[likely]]
#  define MY_UNLIKELY [[unlikely]]
#else
#  define MY_LIKELY
#  define MY_UNLIKELY
#endif

// C++23 attribute
#if __has_cpp_attribute(assume) >= 202207L
#  define MY_ASSUME(expr) [[assume(expr)]]
#else
#  define MY_ASSUME(expr) ((void)(expr))  // best-effort no-op
#endif

The relationship to <version> is that both mechanisms belong to the same portability story: __cpp_lib_* guards library types, __cpp_ guards language constructs, and __has_cpp_attribute guards decoration syntax. A complete compat.hpp typically handles all three layers.

Note that __has_cpp_attribute does not require #include <version>. It is a compiler built-in operator available in any C++11 or later translation unit, similar to __has_include. Vendor-specific attributes (e.g., clang::musttail, gnu::pure) can also be probed with __has_cpp_attribute using the namespaced form __has_cpp_attribute(clang::musttail).

Best Practices

Include <version> as the first header in portability shims. A dedicated compat.hpp or feature_flags.hpp that starts with #include <version> and then sets project-local macros is easier to audit than #ifdef blocks scattered across hundreds of files.

Use #ifdef for existence, #if for revision. Testing #if __cpp_lib_foo >= YYYYMML without an enclosing #ifdef is undefined behaviour if the macro is absent—it silently evaluates to #if 0, which gives the wrong fallback instead of a compilation error. Be explicit:

cpp
#if defined(__cpp_lib_foo) && __cpp_lib_foo >= 202106L
// safe
#endif

Do not conflate <version> with the actual feature headers. Including <version> does not make std::format or std::ranges available. It only tells you whether including <format> or <ranges> is worthwhile. Always include the actual feature header before using its types.

Prefer macros over __has_include for standards compliance guarantees. A header can exist while containing a non-conforming or partial implementation. The __cpp_lib_* value is an implementation's promise that its support meets the standard as of that date. __has_include makes no such promise.

Common Pitfalls

Missing <version> in pre-C++20 code. The header does not exist before C++20. If your project targets C++17 and below, individual feature macros are only available after including their respective headers, and many macros simply do not exist. Guard the include itself:

cpp
#if defined(__cplusplus) && __cplusplus >= 202002L
#  include <version>
#endif

Assuming macro absence means feature absence. Some compilers define __cpp_lib_* macros in individual headers even in C++17 mode as a non-standard extension. <version> being absent does not prove the feature is unavailable; it only proves the centralized header isn't there.

Stale cached values in incremental builds. Build systems that track header dependencies may not re-run feature detection if only a compiler flag changes (e.g., upgrading from -std=c++17 to -std=c++20). A full clean rebuild is necessary after changing the language standard.

Language macros versus library macros. __cpp_constexpr tracks compiler language support for constexpr; __cpp_lib_constexpr_string tracks the C++20 library additions that made std::string operations constexpr. Both may be defined, independently, with different values. Mixing them up produces subtle bugs in conditional code.

See Also