C++98 to C++23 in one file

The language has a version and the program can read it. __cplusplus is a number that identifies the standard the compiler is using, and the __cpp_* macros report individual features that the compiler and the library implement.

The same source compiles under several -std values, and the macros decide which branch is built. Lesson 001 showed the default standard of this compiler; this lesson makes the differences visible one number at a time.

The version macros are also how a library states its requirement: a static_assert on the language version in a header stops a build with one clear message instead of a page of errors from a feature the compiler does not have.

The standard number is also what a package manager records, so a library built for C++17 and one built for C++23 can coexist as separate binaries. That is the deployment answer to the same question.

__cplusplus under every standard

__cplusplus is defined as 201103L for C++11, 201402L for C++14, 201703L for C++17, 202002L for C++20 and 202302L for C++23. The trailing L is there because the value is a long integer.

The program prints the macro, and the five compiles pass a different -std each. The output is the list of values, one per build, which is the quickest way to check which dialect a build script really passes.

The value is the standard the compiler implements, not the one the source would like. A compiler that does not know a standard refuses the flag, and one that knows it partially reports a lower value than the paper's number.

#include <iostream> int main() { std::cout << __cplusplus << '\n'; }

g++ -std=c++11 -Wall -Wextra version.cpp -o v11
./v11
g++ -std=c++14 -Wall -Wextra version.cpp -o v14
./v14
g++ -std=c++17 -Wall -Wextra version.cpp -o v17
./v17
g++ -std=c++20 -Wall -Wextra version.cpp -o v20
./v20
g++ -std=c++23 -Wall -Wextra version.cpp -o v23
./v23

201103
201402
201703
202002
202302

The feature macros

Each feature has a macro such as __cpp_constexpr or __cpp_lib_optional, and the value is the revision of that feature. A branch guarded by #if can use it only when the macro is at least the number the feature needs.

The program prints three of them under C++23: the constexpr revision 202211, the optional revision 202110 and concepts 202002. Those numbers are the compiler's report of what it implements, not of what the standard contains.

The macros come from two places: the compiler for language features and the library headers for library features, which is why __cpp_lib_optional needs <optional> to be included. cppreference lists the values, and a static_assert on one is a portable check.

#include <iostream> #include <optional> int main() { std::cout << __cpp_constexpr << ' ' << __cpp_lib_optional << ' '; std::cout << __cpp_concepts << '\n'; }

g++ -std=c++23 -Wall -Wextra features.cpp -o features
./features

202211 202110 202002

One file, two standards

The preprocessor can choose a branch from __cplusplus, and the same file then compiles under two standards with different behaviour. The program prints a different line for C++20 and later and for everything before it.

Two builds, one with -std=c++17 and one with -std=c++23, print the two lines. The branch is decided at compile time, so the binary contains one of the two strings, not both.

This is how a library supports several standards from one source: a #if around the alternative implementation, and a build that selects it. The alternative is a separate file per standard, which doubles the code and the tests.

#include <iostream> int main() { #if __cplusplus >= 202002L std::cout << "C++20 or later\n"; #else std::cout << "before C++20\n"; #endif }

g++ -std=c++17 -Wall -Wextra branch.cpp -o older
./older
g++ -std=c++23 -Wall -Wextra branch.cpp -o newer
./newer

before C++20
C++20 or later

A feature the standard does not have

A keyword is available only under the standard that introduced it. consteval is C++20, so a C++17 build rejects the declaration and names the flag that would allow it.

The program uses consteval and is compiled with -std=c++17. A warning says the identifier is a keyword in C++20, and the error says that it does not name a type: to a C++17 compiler it is just an unknown name before int.

The fix is the flag, not the code, when the compiler supports the standard. A library that must build on both uses a feature macro around the declaration and provides a constexpr version for the older build.

consteval int answer() { return 42; } int main() { return answer(); }

g++ -std=c++17 -Wall -Wextra consteval17.cpp -o old_answer

consteval17.cpp:1:1: warning: identifier 'consteval' is a keyword in C++20 [-Wc++20-compat]
1 | consteval int answer()
| ^~~~~~~~~
consteval17.cpp:1:1: error: 'consteval' does not name a type; did you mean 'constexpr'?
1 | consteval int answer()
| ^~~~~~~~~
| constexpr
consteval17.cpp: In function 'int main()':
consteval17.cpp:8:12: error: 'answer' was not declared in this scope
8 | return answer();
| ^~~~~~

Exercises

  1. Add C++98 and C++03 to the version program and run both; note which one the compiler still accepts.
  2. Print `__cpp_lib_span` under C++20 and C++23 and explain the difference.
  3. Guard a consteval function with `__cpp_consteval` and build the same file under two standards.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does __cplusplus report?

    Answer

    The standard the compiler is compiling under

    The macro is the language version, and the compiler and library have their own version macros.

  2. What is the value of __cplusplus under C++17?

    Answer

    201703

    The number is the year and month of the standard, 2017 and 03.

  3. Where can __cpp_lib_optional be used?

    Answer

    After including <optional>

    The library feature macros are defined by the headers that provide the feature.

  4. The program is built with g++ -std=c++23 q.cpp. What does it print? #include <iostream> int main() { std::cout << __cplusplus; }

    Answer

    202302

    __cplusplus is 202302 under -std=c++23.

  5. What does this program print under C++23? #include <iostream> int main() { std::cout << __cpp_constexpr; }

    Answer

    202211

    The feature macro reports the constexpr revision the compiler implements.

  6. What does this program print? #include <iostream> int main() { #if __cplusplus >= 202002L std::cout << "C++20 or later"; #else std::cout << "before C++20"; #endif }

    Answer

    C++20 or later

    The macro is 202302, so the first branch is compiled.

  7. Why is the branch decided at compile time?

    Answer

    Because #if is handled by the preprocessor

    The preprocessor removes one branch before the compiler sees it, so only one string is in the binary.

  8. What does the compiler say about consteval under -std=c++17?

    Answer

    That it does not name a type, with a warning that it is a C++20 keyword

    The C++17 parser sees an unknown identifier before the return type, and the warning explains why.

  9. Which macro tells whether a compiler supports a language feature?

    Answer

    __cpp_<feature>

    Each language feature has its own __cpp macro whose value is the revision supported.

  10. Why use a feature macro instead of the standard number?

    Answer

    Because a compiler may support a feature before the whole standard

    A compiler can implement one paper under an older -std, and the feature macro reports exactly that.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev