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++20flag).
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 ofconstexprcapabilities.__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 forstd::format.__cpp_lib_span: Checks for the availability of thestd::spanview.__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';
}
#endifCompanion 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.