Appendix K — The Standards Catalogue
Appendix K — The Standards Catalogue¶
The book pins -std=c++17 and says why in Chapter 10: vendor-SDK work inherits its host's toolset, and C++17 is what that world lets you rely on. Then, forty times across the chapters, it flags a newer spelling — jthread for thread-plus-join, format for the stream dance, expected for the hand-rolled Result — each where the C++17 form is taught. That is the right place for the flag and the wrong place to look one up. This page is the lookup: which standard a feature arrived in, the spelling this book teaches, the spelling a newer codebase uses, and the page that owns it; then the history in one sitting, for the reader who wants to know what "modern C++" is modern relative to; and first, because it decides everything else, how to find out which standard a toolchain is actually speaking.
It is Appendix G and Appendix J's shape — lookup material — with one difference: what a probe can ask, a probe asks. exercises/cookbook/standard.cpp is built by build_all.sh at three standards and refused at a fourth, and by the buildlab-msvc job with and without the switch the first section is about. What no probe asks — the default each compiler picks when nobody writes a switch, and which release of a library first shipped a header — is stated from the vendors' documentation, and re-checks itself only on the day you rely on it.
Which standard am I on¶
In C# the language version is a line in the project file, <LangVersion>, and the compiler that reads it is the one that ships with the SDK — one artifact, one number. C++ has three things that version separately: the standard (an ISO document), the compiler (which implements some of it), and the standard library (a separate project, even when it ships in the same box, which implements the library half on its own schedule). So "which standard am I on" is three questions, and the probe asks all three:
// The standard the compiler was TOLD to speak. On MSVC __cplusplus is
// 199711 unless /Zc:__cplusplus is passed - a lot of code once tested
// it, so the honest value became opt-in - and _MSVC_LANG carries the
// real answer whether or not the switch is on.
std::printf("%-32s %ld\n", "__cplusplus", static_cast<long>(__cplusplus));
#ifdef _MSVC_LANG
std::printf("%-32s %ld\n", "_MSVC_LANG", static_cast<long>(_MSVC_LANG));
#else
std::printf("%-32s absent (not MSVC)\n", "_MSVC_LANG");
#endif
__cplusplus is the year and month of the standard the compiler was asked for — 201703 for C++17, 202002 for C++20, 202302 for C++23 — and the first trap is that MSVC reports 199711 for all of them unless you pass /Zc:__cplusplus, because too much old code tested the macro against that number for Microsoft to change the default. _MSVC_LANG carries the honest value on MSVC either way; a header that must know asks both. The buildlab-msvc job builds the probe twice and asserts both readings, because a vendor default is a fact that can change under a book without telling anyone.
Trap: #if __cplusplus >= 201703L around a C++17 path builds and runs on MSVC with /Zc:__cplusplus off — and takes the other branch, every time, with nothing to report. Guard on the feature's own macro, or on _MSVC_LANG as well; the probe shows the two disagreeing.
The second half of the probe asks the library, and it is the half that matters more:
// Library features: defined by the standard library's headers, so they
// track what THIS library has implemented - which can lag the year on
// the -std= switch by a release or more, and is the reason to ask the
// macro rather than __cplusplus before reaching for a feature.
#ifdef __cpp_lib_optional
report("__cpp_lib_optional", __cpp_lib_optional);
#else
report("__cpp_lib_optional", 0);
#endif
Every feature the standard adds gets a feature-test macro — __cpp_if_constexpr from the compiler for a language feature, __cpp_lib_optional from the library for a library one — whose value is the year and month the feature reached its current shape, and whose absence means the toolchain does not have it whatever __cplusplus says. The <version> header (C++20, but shipped in C++17 mode by all three libraries) carries the library half in one place. The question to ask before reaching for std::expected is therefore not "am I on C++23" but "is __cpp_lib_expected defined" — on the machine this page was written on, -std=c++26 still leaves __cpp_lib_move_only_function absent, while the Linux runner's libstdc++ reports it under -std=c++23 — a fact about two libraries, and no contradiction at all. On this machine, clang with libc++, the probe at the book's floor and at C++23:
$ clang++ -std=c++17 standard.cpp -o s17 && ./s17 | grep -E 'cplusplus|span|expected'
__cplusplus 201703
__cpp_lib_span absent
__cpp_lib_expected absent
$ clang++ -std=c++23 standard.cpp -o s23 && ./s23 | grep -E 'cplusplus|span|expected'
__cplusplus 202302
__cpp_lib_span 202002
__cpp_lib_expected 202211
Three spellings for the same switch, and one CMake pair that produces all of them: -std=c++17 on clang and GCC, /std:c++17 on MSVC, and set(CMAKE_CXX_STANDARD 17) with CMAKE_CXX_EXTENSIONS OFF (Chapter 26), which writes -std=c++17 rather than the gnu++17 dialect. The defaults, when nobody writes a switch: GCC 11 through 15 default to gnu++17 and GCC 16 to gnu++20; clang has defaulted to gnu++17 since clang 16; MSVC defaults to /std:c++14. Which is the whole argument for writing the switch down: a codebase that relies on the default is on a standard chosen by whichever compiler the next person installs.
Key principle: "The standard a codebase speaks is a line in its build description, not a fact about its compiler — and whether a feature is there is its feature-test macro's answer, not __cplusplus's, because the library ships behind the language."
The probe's first act is not a print statement but a refusal, and build_all.sh asks -std=c++14 to build it so the refusal is asserted rather than assumed:
#ifdef _MSVC_LANG
#define STANDARD_SPOKEN _MSVC_LANG
#else
#define STANDARD_SPOKEN __cplusplus
#endif
static_assert(STANDARD_SPOKEN >= 201703L, "this book's floor is C++17: pass -std=c++17 or /std:c++17");
Five lines at the top of a project's one common header do the same job for a codebase: the floor, stated once, enforced by the compiler in a sentence instead of by whichever optional first fails to be found three headers deep — Chapter 41's judge, applied to the toolchain. The #ifdef is not decoration. The first draft of this probe asserted __cplusplus alone, and the buildlab-msvc job refused it at /std:c++17 with the assertion's own sentence — the trap of the previous section, met before main ran, on the first build. And the Linux leg of the same run added a second lesson the section above only implied: GCC 13 reports __cplusplus as 202100 for -std=c++23, not 202302, because the value is the compiler's opinion of a standard's date at the time the compiler shipped, and C++23 was not final until after GCC 13 was. Two compilers, one switch, two numbers — the year is a weak key, and the feature-test macro is the strong one.
The features, by standard¶
The spelling this book teaches is the C++17 one, and the newer spelling column is what changes in a codebase past the floor — nothing else moves. Where a row's newer spelling has a feature-test macro, it is named, so the probe above answers whether a given toolchain has it.
C++11 and C++14 — the ground the whole book stands on. Used without comment from Chapter 1; listed so a reader who meets pre-2011 code knows what is missing there.
| Feature | Arrived | Owned by |
|---|---|---|
auto, lambdas, range-for |
C++11 | Chapter 10, Chapter 2 |
unique_ptr, shared_ptr, weak_ptr (make_unique is C++14) |
C++11 | Chapter 1 |
rvalue references, std::move, the move operations |
C++11 | Chapter 6 |
nullptr, enum class, override, final, = default, = delete, constexpr, static_assert |
C++11 | Chapter 10, Appendix A |
| inline namespaces (the version in the mangled symbol) | C++11 | Chapter 12, Chapter 30 |
[[noreturn]] (C++11) and [[deprecated]] (C++14) — the rest of the set is C++17, below |
C++11 | Recipe 51 |
| member initializers in the class body, delegating constructors | C++11 | Chapter 4 |
<thread>, <mutex>, <atomic>, <condition_variable>, <future> |
C++11 | Chapter 29, Recipe 13 |
<chrono> |
C++11 | Recipes 6, 28–30 |
| variadic templates | C++11 | Chapter 41 |
init-captures ([v = std::move(big)]), generic lambdas (auto parameters), decltype(auto), the _t trait aliases (std::decay_t) |
C++14 | Chapter 22, Chapter 19, Chapter 41 |
C++17 — the floor. Everything here is spelled as taught, everywhere in the book.
| Feature | Owned by |
|---|---|
std::optional, std::variant and std::visit, std::string_view, std::any |
Chapter 10, Recipes 19, 20 |
std::filesystem |
Recipes 10–12, 38–40 |
| structured bindings | Chapter 10, Recipe 35 |
if constexpr, fold expressions, class template argument deduction, std::void_t |
Chapter 41 |
inline variables and inline static members |
Chapter 12, Chapter 14 |
| mandatory copy elision for a returned temporary | Chapter 6, Chapter 14 |
std::from_chars / std::to_chars |
Recipe 19; the double overloads came later to every library — libstdc++ 11, MSVC 2019 16.4, libc++ 20, and on Apple's libc++ only for a deployment target of macOS 26 or later — so Chapter 42's number parse carries an #if |
std::invoke, std::invoke_result_t |
Recipe 28, Chapter 38 |
std::scoped_lock, std::shared_mutex |
Chapter 29 |
[[maybe_unused]], [[nodiscard]], [[fallthrough]] |
Recipe 24, Recipe 51 |
enable_shared_from_this throwing bad_weak_ptr on an unowned object |
Chapter 1 |
removed: dynamic exception specifications (throw(A, B)), std::auto_ptr; deprecated: <codecvt> |
Chapter 8, Recipe 17 |
C++20 — the spellings a newer codebase changes.
| The book teaches | The C++20 spelling | Macro | Owned by |
|---|---|---|---|
a std::thread joined by hand, or in a destructor |
std::jthread, std::stop_token |
__cpp_lib_jthread |
Chapter 29, Recipe 16 |
| the erase-remove idiom | std::erase_if |
__cpp_lib_erase_if |
Chapter 11 |
m.find(k) != m.end() |
m.contains(k) |
— (no macro; test the year, >= 202002L, through _MSVC_LANG on MSVC) |
Chapter 11 |
streams and snprintf |
std::format |
__cpp_lib_format |
Recipes 5, 29 |
| a pointer plus a length | std::span |
__cpp_lib_span |
Chapter 10, Appendix H |
find_if with a lambda, the map-to-vector dance |
ranges and views | __cpp_lib_ranges |
Chapter 10, Chapter 19 |
the detection idiom, static_assert on a trait |
concepts and requires |
__cpp_concepts |
Chapter 7, Chapter 41 |
__FILE__ and __LINE__ through a macro |
std::source_location |
__cpp_lib_source_location |
Chapter 28 |
| shifts at documented offsets | std::endian, std::bit_cast — the advice does not change |
__cpp_lib_endian, __cpp_lib_bit_cast |
Chapter 34 |
size_t loops and < comparisons |
std::ssize, std::cmp_less |
__cpp_lib_ssize, __cpp_lib_integer_comparison_functions |
Appendix A |
u8path |
path(u8"...") with char8_t |
__cpp_char8_t |
Recipe 10 |
localtime_r and an offset you read yourself |
std::chrono::zoned_time, clock_cast |
__cpp_lib_chrono at 201907 or later — every C++17 library defines it at 201611, so its presence says nothing |
Recipes 6, 29, 39 |
#include |
modules (import) — headers everywhere in SDK work regardless |
__cpp_modules (which clang does not define, and GCC only under -fmodules) |
Chapter 12 |
| a queue and a thread | coroutines — a library on top before they are usable | __cpp_impl_coroutine |
Chapter 29 |
hand-written == and < |
operator<=>, defaulted comparisons |
__cpp_impl_three_way_comparison |
— |
Config c; c.timeout = 30; |
designated initializers, Config{.timeout = 30} |
__cpp_designated_initializers |
— |
C++23 — the ones the book names, each with a check-your-standard flag.
| The book teaches | The C++23 spelling | Macro | Owned by |
|---|---|---|---|
Result<T, E> over a variant |
std::expected, and_then, transform |
__cpp_lib_expected |
Chapter 8, Recipe 22 |
if (!opt) return nullopt; |
optional::and_then, transform, or_else |
__cpp_lib_optional at 202110 or later |
Chapter 10, Recipe 19 |
auto for a move-only lambda |
std::move_only_function |
__cpp_lib_move_only_function |
Chapter 22 |
text.find(word) != npos |
text.contains(word) |
__cpp_lib_string_contains |
Recipe 18 |
| a cast over a mapped region | std::start_lifetime_as |
__cpp_lib_start_lifetime_as |
Recipe 43 |
dependent_false_v<T> |
static_assert(false) in a discarded branch |
— (language; clang 17, GCC 13) | Chapter 41 |
| — | std::print and std::println, std::stacktrace, std::flat_map, std::mdspan, deducing this |
__cpp_lib_print, __cpp_lib_stacktrace, __cpp_lib_flat_map, __cpp_lib_mdspan, __cpp_explicit_this_parameter |
— |
C++26 — in flight as this is written. Two rows the book already cites: hardened library preconditions, the standardised form of Chapter 13's _LIBCPP_HARDENING_MODE and _GLIBCXX_ASSERTIONS, and std::optional<T&> (Chapter 10; no macro of its own — __cpp_lib_optional at 202506 or later, the probe's __cpp_lib_optional line read for its value). The headline features — static reflection, contracts, std::execution — none of which this book leans on, arrive in compilers piecemeal over the next several releases, which is the paragraph below in miniature.
The standards in one sitting¶
C# ships a language, a compiler and a runtime together, roughly yearly, and the version you are on is the SDK you installed. C++ is an ISO standard with a three-year cadence since 2011, implemented by three compilers and three standard libraries on their own schedules — so "C++20" names a document, and whether you can use it is a fact about the toolchain in front of you.
C++98 and C++03. The first standard, and a 2003 corrigendum that changed almost nothing a user would notice. This is the C++ a returning developer remembers: new and delete by hand, auto_ptr as the only smart pointer and a broken one, no lambdas, no auto, NULL, and iterators spelled out in full. Code written for it still exists in every vendor SDK older than a decade, and the Bestiary's C-flavoured shapes (Chapter 16) are partly a legacy of an era when the language offered nothing better across a binary boundary.
C++11 — the watershed. Delayed so long it was called C++0x through most of its development, and the release "modern C++" is modern relative to: move semantics, unique_ptr and shared_ptr, lambdas, auto, range-for, nullptr, constexpr, threads and atomics in the standard library, and a memory model that made Chapter 29's data race a defined concept at all. Most of Appendix B is spelled in C++11. A codebase that predates it is a different language wearing the same syntax, and modernising one is a subject of its own rather than this page's.
C++14. The bug-fix release: make_unique (forgotten in C++11), generic lambdas, init-captures, relaxed constexpr. Small enough that "C++11/14" is one era in conversation.
C++17 — this book's floor. optional, variant, string_view, filesystem, structured bindings, if constexpr, guaranteed elision, inline variables: the release after which everyday code stopped needing Boost for the basics. The floor for the reason Chapter 10 gives — the oldest thing you must link against sets it, and a vendor SDK's pinned toolchain is that thing — and the floor most maintained SDKs now state themselves, which is why the pin is a policy rather than a compromise.
C++20 — the big one after 11. Concepts, ranges, coroutines, modules, format, span, jthread, <=>, and the feature-test macros standardised in <version>. Complete in all three compilers for years now, and the default dialect of GCC 16 — but coroutines need a library on top and modules need build-system support that is still arriving, so "on C++20" usually means the library half and concepts, not the whole document.
C++23. expected, print, monadic optional, flat_map, mdspan, deducing this, and std::start_lifetime_as. Published in 2024, and the library half is where a toolchain lags most visibly: a compiler accepting -std=c++23 and a library missing <expected> is a normal state of affairs, which is why the probe above asks the library rather than the switch.
C++26. Being finalised as this book is written. Static reflection and contracts are the headline; the hardened library is the row a plug-in author meets first. Nothing here changes a lesson in this book; the spellings will move again, and a reader on a C++26 toolchain in 2029 reads the C++17 forms here the way a reader today reads auto_ptr — recognisable, superseded, and still in the vendor's sample code.
The cadence has a consequence for the reader's own code that C# never had: a toolchain upgrade is a standard library upgrade too, and the two can move separately. A new compiler with the old library, or the reverse, is how __cplusplus and __cpp_lib_expected come to disagree — and why the probe asks both.
Moving a codebase past the floor¶
When a team raises CMAKE_CXX_STANDARD from 17 to 20, the rows above are the diff. Nothing in the C++17 spellings stops compiling — the standard removes almost nothing — so the change is a flag, a build, and a reading of the warnings. What to reach for, and what not to:
| You need | Reach for | Think twice about |
|---|---|---|
| To know what standard a codebase speaks | its build description — CMAKE_CXX_STANDARD, -std=, /std: — then the probe |
the compiler's version, or its default |
| To know whether the library has a feature | its feature-test macro through <version> |
__cplusplus, which the library never reads |
expected on a C++17 codebase |
Recipe 22's Result<T, E>, or a header-only backport as a Chapter 27 dependency |
raising the standard for one type |
format before C++20 |
fmt, the library std::format was standardised from, under Chapter 27's strategies |
streams in a hot loop |
span before C++20 |
the pointer-plus-length pair, Chapter 10 | writing a span type of your own |
jthread before C++20 |
a thread whose owner joins it in a destructor, Recipe 16 | detach |
| Concepts before C++20 | the detection idiom and a static_assert, Chapter 41 |
enable_if gymnastics |
| To raise a codebase's floor | flip the switch, build clean, then adopt spellings where they read better | rewriting working code to look newer |
| A vendor sample that is C++98 | read it as the Bestiary shape it is, wrap it in this book's spellings at the boundary | modernising the vendor's code |