Understanding C++ Feature Test Macros

Feature test macros in C++ provide a standardized mechanism for querying whether a compiler and standard library implementation support specific language or library features. Introduced formally in C++20 and rooted in the earlier SD-6 technical report, these macros enable developers to write portable, cross-platform code that adapts to varying levels of language support across different toolchains. This article explains the purpose of feature test macros, how they solve traditional compiler-detection problems, and how to effectively use them in modern C++ projects.

The Problem with Traditional Compiler Detection

Historically, C++ evolves in three-year cycles (C++11, C++14, C++17, C++20, and C++23), but compiler vendors implement features incrementally. Before standardized feature test macros, developers had to rely on checking compiler-specific predefined macros such as __GNUC__, _MSC_VER, or __clang_major__.

This approach was brittle and difficult to maintain because:

  • Feature availability varies across minor compiler updates.
  • Different compilers implement features at different rates.
  • Checking vendor versions does not guarantee that a specific language feature is fully implemented or enabled (for instance, via a -std=c++20 flag).

The Purpose of Feature Test Macros

Feature test macros eliminate compiler-specific checks by shifting the focus from which compiler is running to which capability is available. Each macro evaluates to an integer representing the year and month (in YYYYMM format) when a specific feature or revision was voted into the ISO C++ standard.

If a macro is defined, the feature is supported. If a feature receives updates in later standards, the macro's integer value increases, allowing code to target specific revisions of that feature.

Categories of Feature Test Macros

Feature test macros fall into two primary categories:

1. Core Language Feature Test Macros

Core language macros indicate support for syntactic and semantic language features, such as concepts, structured bindings, or constexpr extensions. These macros do not require including any header files; they are predefined by the preprocessor when the appropriate language mode is enabled.

Examples include:

  • __cpp_constexpr: Tracks the evolution of constexpr capabilities.
  • __cpp_concepts: Indicates support for C++20 concepts.
  • __cpp_structured_bindings: Indicates support for structured binding declarations.

2. Library Feature Test Macros

Library macros verify the availability of components within the C++ Standard Library, such as std::span, std::format, or std::expected.

To check for library capabilities without incurring heavy compilation overhead, C++20 introduced the <version> header. Including <version> defines all supported library feature macros without pulling in entire implementations like <vector> or <algorithm>.

Examples include:

  • __cpp_lib_format: Indicates standard library support for std::format.
  • __cpp_lib_span: Checks for the availability of the std::span view.
  • __cpp_lib_jthread: Verifies support for cooperative cancellation threads.

Conditional Compilation and Fallbacks

The primary application of feature test macros is conditional compilation using #if or #ifdef directives. This allows codebases to adopt modern features while retaining backward compatibility with older toolchains or fallback implementations.

#include <version>

#if defined(__cpp_lib_format) && __cpp_lib_format >= 201907L
    #include <format>
    template <typename... Args>
    void log_message(std::string_view fmt, Args&&... args) {
        std::print("{}", std::vformat(fmt, std::make_format_args(args...)));
    }
#else
    #include <iostream>
    template <typename... Args>
    void log_message(const char* msg, Args&&...) {
        // Fallback for compilers without std::format support
        std::cout << msg << '\n';
    }
#endif

Companion Preprocessor Operators

In addition to named macros, modern C++ provides preprocessor operators that complement feature testing:

  • __has_include(<header>): Checks whether a header file exists before attempting to include it, preventing compilation errors on incomplete library implementations.
  • __has_cpp_attribute(attribute): Tests whether the compiler supports a standard or vendor-specific attribute (such as [[nodiscard]] or [[likely]]).

Conclusion

Feature test macros replace complex, error-prone compiler version checks with clear, standard-defined capability tests. By querying core language macros, using the lightweight <version> header, and employing companion operators like __has_include, C++ developers can build robust libraries and applications that seamlessly take advantage of new standards while maintaining broad portability.