C++20 Course

GCC 16 Defaults to C++20

GCC 16 compiles C++ as C++20 unless a standard is pinned. Code that relied on the old C++17 default can fail in several distinct ways, and pinning does not fix all of them. This guide proves the flip, pins the standard, and works through each breakage class with the actual compiler output.

Prove the flip

GCC 16 is released. The GCC 16 release page lists GCC 16.1 on 30 April 2026 and GCC 16.2 on 7 August 2026, and no later release was listed when this guide was checked on 4 November 2026. The GCC 16 changes page states the default change in one passage:

C++20 by default: GCC 16 changes the default language version for C++ compilation from -std=gnu++17 to -std=gnu++20. If your code relies on older versions of the C++ standard, you will need to either add -std= to your build flags, or port your code; see the porting notes.

Do not take that passage on trust for your own machine. Prove it with the __cplusplus macro, which reports the standard in effect:

#include <cstdio>
int main() { std::printf("%ld\n", (long)__cplusplus); return 0; }

Compiled with no -std= flag, the program prints the following on the build box used for this guide:

Compiler, no -std= flag __cplusplus Effective default
GCC 13.3.0 (system compiler) 201703 C++17
GCC 14.2.0 201703 C++17
GCC 16.2.0 202002 C++20
GCC 16.2.0 with -std=gnu++17 201703 C++17
Clang 18.1.3 201703 C++17, unchanged

The last two rows are the two facts the rest of this guide turns on. The pin restores the old default on GCC 16, and Clang 18 did not change its default, so a codebase that builds with both compilers breaks on one of them first. GCC 15 was not installed on the build box and was not tested; nothing in this guide claims a GCC 15 result. Where the checklist below calls for a GCC 15 and GCC 16 build matrix, that is the matrix to run, not a result already obtained.

The decision at this point is pin or port. Pinning buys time and keeps the old behavior. Porting removes the debt but takes the full checklist below. Either way, pin first: a pinned build is the known-good baseline that every later fix is measured against. The checklist is ordered so that each step leaves the build in a working state.

This guide is standalone in the course. It does not depend on the earlier guides, but two of them are directly relevant to what the new default unlocks. C++20 modules are still experimental in GCC 16 and need -fmodules; see C++20 Modules with CMake 3.28 and Ninja. The C++23 library features the new default makes reachable, including std::expected pipelines and std::mdspan views, are covered in C++23 std::expected Error-Handling Pipelines and C++23 std::mdspan and Deducing This. The modules guide needed GCC 14 or newer; the mdspan guide needed GCC 16.

Pin first

Pinning means writing the standard into the build definition, so every compile invocation gets it. The direct flag is -std=gnu++17 for the GNU dialect or -std=c++17 for the strict dialect. The probe above shows the flag works on GCC 16.2.0: __cplusplus returns to 201703. A flag typed by hand at one terminal does not pin anything; the build definition is the pin.

For CMake, the pin is two lines in CMakeLists.txt. This pair was tested on the build box with GCC 16.2.0 as the compiler. Without the pin, the build fails on the keyword program from the next section:

cmake_minimum_required(VERSION 3.28)
project(pincheck CXX)
add_executable(pincheck main.cpp)

With the pin, the same source builds and runs:

cmake_minimum_required(VERSION 3.28)
project(pincheck CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(pincheck main.cpp)

CMAKE_CXX_STANDARD_REQUIRED ON matters: without it, CMake silently falls back to an older standard if the requested one is unavailable, and the build then tests something other than the pinned standard. Keep the pin next to the project declaration, where the next person to read the file sees it before any target.

One trap in this step comes from the build system itself, not from the code. The porting page reports that Autoconf before release 2.73 adds a spurious -std=gnu++11 option to Makefiles when it fails to verify C++11 support, producing this error:

error: 'make_unique' is not a member of 'std'

If that error appears, look at the actual compile line before touching the code. A -std=gnu++11 flag in the line is the Autoconf bug, not a code breakage. The porting page's workaround is to remove the AC_PROG_CXX directive from configure.ac and regenerate the Makefiles. Check the Autoconf version in the same pass: anything before 2.73 carries this trap.

Document every pin when it is added, with the reason and a retirement date. A pin with no retirement date becomes the next default change's surprise. The ship checklist in the last section returns to this.

Language breakages

These breakages come from C++20 rule changes. Pinning to C++17 restores the old behavior for all of them, which is what makes the pin a useful first step. Each program below was compiled with GCC 13.3.0, GCC 14.2.0, and GCC 16.2.0 at their defaults, and with GCC 16.2.0 plus -std=gnu++17. The failing programs are labeled as failing, and the error text is quoted from the actual output.

concept and requires are keywords now

C++20 defines concept and requires as keywords, so they can no longer be used as identifiers. This program compiled on GCC 13 and GCC 14 at their defaults and fails on GCC 16 at its default:

int concept = 3;
int main() { return concept - 3; }

GCC 16.2.0 rejects it with:

error: C++20 concept definition syntax is ‘concept <name> = <expr>’

The same failure occurs for requires as an identifier:

int requires = 2;
int main() { return requires - 2; }

GCC 16.2.0 rejects it with:

error: expected unqualified-id before ‘requires’

The fix is to rename the identifier. Renaming concept to concept_count compiles on GCC 16 at its default and runs. There is no flag that restores identifiers: the only alternatives are the rename or the pin. Search the whole codebase for both words as identifiers before fixing one at a time; a second use in another file produces the same error after the first is fixed, and a rename pass that stops at the first file wastes a build cycle.

operator!= ambiguity

In C++20, a type that defines operator== gets an operator!= provided implicitly by the compiler, through rewriting. The porting page warns that this can cause ambiguities when the type also defines its own operator!= with an unconventional signature, and gives this example error:

error: ambiguous overload for 'operator!=' (operand types are 'daeStringRef' and 'long int')

That error is quoted from the porting page, not from a program on the build box. The ordinary forms tried here did not reproduce it: a hidden-friend operator!= with the same signature as operator==, a member operator!= with the same signature, and four further shapes with non-const and mixed-type signatures all compiled on GCC 16.2.0 and ran. The program below is one of the forms that compiled, and it prints 1:

#include <cstdio>
struct X {
  int v;
  friend bool operator==(const X& a, const X& b) { return a.v == b.v; }
  friend bool operator!=(const X& a, const X& b) { return a.v != b.v; }
};
int main() {
  X a{1}, b{2};
  std::printf("%d\n", (a != b) ? 1 : 0);
  return 0;
}

The lesson is narrow: do not assume every hand-written operator!= breaks, and do not assume none does. The ambiguity needs the unconventional signature the porting page describes, such as a comparison that only accepts non-const arguments. When the error appears, fix the signatures first: make the comparison accept const arguments, or remove the hand-written operator!= and let the rewritten form do the work. Removing it from the program above compiles on GCC 16 at its default and still prints 1, because a != b rewrites to !(a == b). The porting page notes that fixing argument-type signatures is often the simplest fix, and more complicated cases may need overloads added or removed.

char8_t literals

In C++20, u8"..." literals changed type from const char* to const char8_t*. This program compiled on GCC 13 and GCC 14 at their defaults and fails on GCC 16 at its default:

#include <cstdio>
const char* s = u8"hello";
int main() { std::printf("%s\n", s); return 0; }

GCC 16.2.0 rejects it with:

error: invalid conversion from ‘const char8_t*’ to ‘const char*’ [-fpermissive]

The porting page's fix is a cast from the u8 literal to plain char or char const*. The cast form below compiles on GCC 16 at its default and prints hello:

#include <cstdio>
const char* s = reinterpret_cast<const char*>(u8"hello");
int main() { std::printf("%s\n", s); return 0; }

Prefer retyping the variable to const char8_t* where the code handles UTF-8 text and the rest of the call chain accepts it. The cast is the right fix where the variable feeds an API that takes const char* and cannot change. Either way, grep for u8" and u8' across the codebase: the literal form, not the variable name, is what to search for.

[=] capture of this is deprecated

C++20 deprecates implicitly capturing this in a lambda with [=] when the lambda body uses member names. This is a warning, not an error, and the program still runs. Compiled with -Wall, GCC 13.3.0 at its default is silent and GCC 16.2.0 at its default warns:

#include <cstdio>
struct C {
  int v = 7;
  auto make() { return [=] { return v; }; }
};
int main() {
  C c;
  std::printf("%d\n", c.make()());
  return 0;
}

GCC 16.2.0 with -Wall prints:

warning: implicit capture of ‘this’ via ‘[=]’ is deprecated in C++20 [-Wdeprecated]

The fix is to name the capture explicitly. Adding this to the capture list compiles on GCC 16 at its default with -Wall and no warning, and still prints 7:

auto make() { return [=, this] { return v; }; }

Choose [=, this] when the lambda needs the object alive through the call, and [=, *this] when it needs a copy. The porting page gives both forms. A warning-only breakage is easy to defer and easy to regret: treat the first warning-free build as the gate, not the first build that links.

operator>> into a char*

The operator>> overload for reading from a stream into a char* was removed in C++20, because it offers no way to prevent buffer overrun. The pointer form below fails on GCC 16 at its default and compiled on GCC 13 at its default:

#include <cstdio>
#include <sstream>
int main() {
  std::istringstream in("hello");
  char* buf = new char[16];
  in >> buf;
  std::printf("%s\n", buf);
  delete[] buf;
  return 0;
}

GCC 16.2.0 rejects it with:

error: no match for ‘operator>>’ (operand types are ‘std::istringstream’ {aka ‘std::__cxx11::basic_istringstream<char>’} and ‘char*’)

Note the boundary carefully: reading into a character array, char buf[16]; in >> buf;, still compiled on GCC 16.2.0 in the tests for this guide, because the library provides an array overload. Only the pointer form is removed. The fix the porting page recommends is to read into a std::string, which grows as needed. The string form below compiles on GCC 16 at its default and prints hello:

#include <cstdio>
#include <sstream>
#include <string>
int main() {
  std::istringstream in("hello");
  std::string word;
  in >> word;
  std::printf("%s\n", word.c_str());
  return 0;
}

Reading into a native array is the other fix the porting page gives, using the bound to prevent overrun. Either fix removes the unbounded read, which is the point of the removal.

Library breakages

These breakages come from the standard library, not the language. The distinction matters for the order of fixes: library breakages are fixed by changing which library facility the code calls, and the pin restores the old behavior only where the library still provides the old facility in C++17 mode.

Removed std::allocator members

In C++20, many deprecated members of std::allocator were removed, because std::allocator_traits has provided better defaults since C++11. This program compiled on GCC 13 and GCC 14 at their defaults and fails on GCC 16 at its default. The pin restores it: the same program compiled on GCC 16.2.0 with -std=gnu++17, because the library still provides the members in C++17 mode. The traits fix below is still the durable one, since the members are gone in C++20 mode.

#include <cstdio>
#include <memory>
int main() {
  std::allocator<int> a;
  int* p = a.allocate(1);
  a.construct(p, 42);
  std::printf("%d\n", *p);
  a.destroy(p);
  a.deallocate(p, 1);
  return 0;
}

GCC 16.2.0 at its default rejects it with two errors:

error: ‘class std::allocator<int>’ has no member named ‘construct’
error: ‘class std::allocator<int>’ has no member named ‘destroy’

The removed members named on the porting page are pointer, const_pointer, reference, const_reference, construct, destroy, rebind, is_always_equal, and max_size. The fix is std::allocator_traits in place of the direct member calls. The traits form below compiles on GCC 13, GCC 14, and GCC 16 at their defaults and prints 42:

#include <cstdio>
#include <memory>
int main() {
  std::allocator<int> a;
  int* p = std::allocator_traits<std::allocator<int>>::allocate(a, 1);
  std::allocator_traits<std::allocator<int>>::construct(a, p, 42);
  std::printf("%d\n", *p);
  std::allocator_traits<std::allocator<int>>::destroy(a, p);
  std::allocator_traits<std::allocator<int>>::deallocate(a, p, 1);
  return 0;
}

Custom allocators need the same treatment: any allocator that relied on the removed std::allocator members, or that defined them itself for the old interface, should be rewritten against allocator_traits. Search for .construct(, .destroy(, and ::rebind in allocator code, not just in std::allocator uses.

Missing headers

Programs sometimes use a name without including the header that declares it, relying on the name arriving incidentally through some other header. That reliance breaks when the other header stops declaring it. This program fails on every compiler tested for this guide, GCC 13, GCC 14, and GCC 16 alike, with the same error:

int main() {
  std::uint64_t x = 1;
  return (int)x - 1;
}
error: ‘uint64_t’ is not a member of ‘std’

Including the header fixes it on all three compilers:

#include <cstdint>
int main() {
  std::uint64_t x = 1;
  return (int)x - 1;
}

This class is not specific to the default flip, and the guide does not claim otherwise. It belongs in the checklist because a compiler upgrade is when incidental inclusions change, and the fix is the same in every case: include the header that declares the name. The porting page notes that the compiler may suggest the header to include. Treat any error of the form "was not declared in this scope" as a missing-header candidate before treating it as a language breakage.

ABI boundaries and the rebuild rule

Some C++20 components changed ABI in GCC 16. The changes page lists atomic wait and notify functions, semaphore types, <syncstream> synchronization, the representation of std::format arguments and std::formatter specializations, the representation of std::partial_ordering, the semantics of std::variant with std::jthread, std::stop_token, and std::stop_source alternatives, and the representation of some range components in <ranges>. It adds that the list is not necessarily complete, and that programs using C++20 components should assume those components are not compatible with older releases, because C++20 support was experimental before GCC 16.

The std::variant case has a named escape hatch: the previous behavior can be restored by defining _GLIBCXX_USE_VARIANT_CXX17_OLD_ABI, and the changes page states that this impacts only C++17 mode. Do not use the macro as a general policy. The action for shipped libraries is to rebuild every component with GCC 16 and to stop mixing objects built under the experimental C++20 support with objects built under GCC 16. This guide contains no ABI demo: an ABI break shows up at link time or run time in a multi-library build, not in a single-file program, and a single-file demo would prove nothing. The checklist records the rebuild as a step with a named owner, not as an assumption.

Warnings and diagnostics

This section is the classification that saves the most time. The breakages above are language and library changes: pinning restores the old behavior, and porting removes the need for the pin. The changes in this section are GCC 16 changes. Pinning does not fix them, and porting does not remove them. Fixing them in the wrong bucket, by pinning harder, changes nothing.

New warnings under -Wall and -Wextra

Since GCC 16, the -Wunused-but-set-* warnings have multiple levels. The porting page states that older GCC only implemented what has become level 1, while the new default is level 3, and the new defaults are also used when the warnings are enabled by -Wall or -Wextra. Level 2 ignores pre- and post-increments and decrements; level 3 also ignores compound assignments when the left-hand variable is not also used on the right-hand side. This program is silent under -Wall on GCC 13.3.0 and GCC 14.2.0 at their defaults, and warns on GCC 16.2.0:

int main() {
  int d = 0;
  d += 4;
  return 0;
}

GCC 16.2.0 with -Wall prints:

warning: variable ‘d’ set but not used [-Wunused-but-set-variable=]

The same warning appears on GCC 16.2.0 with -std=gnu++17, which proves the classification: this is a compiler-version change, not a standard change. The pin does not silence it. Compiling with -Wunused-but-set-variable=1 restores silence on GCC 16.2.0 in the tests for this guide, and the porting page gives the other two remedies: remove the newly diagnosed variable, or make it used, for example by casting to (void). The porting page also warns that an unused variable may indicate a coding error, where some other object was mistakenly used in its place. Read each new warning before silencing it; a warning that names a real bug is the cheapest bug report the migration produces.

Expect the error messages themselves to look different. The changes page notes that C++ error messages now have a hierarchical structure, presented with indentation and bullet points, and that the old behavior can be restored with -fno-diagnostics-show-nesting or -fdiagnostics-plain-output. The long operator>> error quoted above shows the new nesting: one headline error followed by numbered candidates. Do not restore the old format to make diffs match; learn the new layout once, since every later GCC 16 error arrives in it.

Machine-readable diagnostics move to SARIF

The json format for -fdiagnostics-format= has been removed in GCC 16. The changes page states that users seeking machine-readable diagnostics should use SARIF. On GCC 16.2.0, the old flag is rejected outright:

g++-16: error: unrecognized argument in option ‘-fdiagnostics-format=json’
g++-16: note: valid arguments to ‘-fdiagnostics-format=’ are: sarif-file sarif-stderr text

The two SARIF forms behave differently, and both were tested on GCC 16.2.0 with a program containing an error. -fdiagnostics-format=sarif-file writes a <source>.sarif file next to the source and leaves stderr empty. -fdiagnostics-format=sarif-stderr writes the SARIF JSON document to stderr. A CI parser that reads JSON diagnostics from stderr needs the sarif-stderr form and a SARIF parser, not a JSON-diagnostics parser with a renamed flag: the document shape is SARIF, with its own schema, version, and runs array. Update the parser and the flag in the same change, and keep one build that still prints text diagnostics for a human to read during the migration.

The MSVC corner, fenced

This section is about Visual Studio only. Nothing in it is a GCC fact, and no GCC claim in this guide comes from the source used here. The Microsoft Learn page on deprecated and removed C++ features is a list that does not claim to be exhaustive, covering Visual Studio 2019 and later. For Visual Studio 2026 (MSVC 14.51), it lists the experimental coroutine headers <experimental/coroutine>, <experimental/generator>, and <experimental/resumable> as deprecated, with a transition to standard C++20 coroutines using the <coroutine> header. In a separate Visual Studio 2026 section, it lists the MSVC compiler switch /await as deprecated and to be removed in a future release, with a transition to standard coroutines using <coroutine>, and /await:strict not deprecated for C++14 and C++17 projects.

The fence matters because the two compilers are migrating on different schedules. A codebase that builds with both GCC and MSVC needs both checklists, run separately, with the MSVC items fixed against the Microsoft Learn page and the GCC items fixed against the GCC pages. Do not carry a deadline, a version number, or a flag from one compiler's list into the other's.

Ship checklist

Work the checklist in order. Each step has a done condition; a step is not done when its errors merely moved to a later file.

Step Action Done when
1 Prove the flip on the build machines with the __cplusplus probe, for every compiler in the matrix. The probe output is recorded for each compiler, including any compiler whose default did not change.
2 Pin the standard in the build definition, with a reason and a retirement date on each pin. The pinned build passes, and the unpinned build's failures are recorded as the porting backlog.
3 Fix the language breakages in porting-page order: keywords, operator!= signatures, char8_t, [=] capture, operator>> into char*. Each class has a fixed program that compiles at the GCC 16 default, or a recorded reason it does not apply.
4 Fix the library breakages: allocator_traits for removed allocator members, missing headers, and the rebuild rule for ABI boundaries. No removed member or missing header remains, and every shipped library is rebuilt with GCC 16.
5 Fix the GCC-only changes: new warnings at their new levels, and the SARIF migration for CI parsers. The build is warning-free at the chosen warning level, and the CI parser consumes SARIF.
6 Run the MSVC corner separately, against the Microsoft Learn page. The Visual Studio build passes with no deprecated-header or /await use remaining.
7 Run the GCC 15 and GCC 16 matrix, and re-check the GCC release page for a release after 16.2. Both compilers pass the pinned and the ported builds, and the tested version is stated in the build record.

Two standing cautions from the research behind this guide. First, the spike decays: as GCC 16 becomes the ambient default, the checklist stops being news and becomes ordinary maintenance, so ship the port promptly rather than treating the pin as the finished state. Second, the demand caveat that applies to this whole course: no Search Console demand evidence exists for programming content on this site, so this guide claims no search demand. It claims only what it tested: the default changed, the breakages above are real on GCC 16.2.0, and the fixes above compile and run.

Frequently asked questions

Is GCC 16 actually released?

Yes. GCC 16.1 was released on 30 April 2026 and GCC 16.2 on 7 August 2026, according to the GCC 16 release page, re-checked on 4 November 2026 with no later release listed. Any premise that GCC 16 is still in development is wrong and should not be repeated.

Should I pin or port?

Pin first in every case, because the pin gives a known-good baseline. Port after, in the checklist order, unless the codebase is small enough that the full port is one sitting. A pin with no retirement date is a deferred port, not a finished migration.

Does pinning fix the new warnings?

No. The -Wunused-but-set-* level change and the SARIF diagnostics change are GCC 16 changes, not C++20 changes. The warning test in this guide warns on GCC 16.2.0 with -std=gnu++17 set. Fix warnings and diagnostics in their own step.

What about GCC 15?

GCC 15 was not installed on the build box and was not tested for this guide. The checklist calls for a GCC 15 and GCC 16 matrix as a step to run, not as a result. Do not quote a GCC 15 outcome from this guide.

Does this guide cover MSVC?

Only in the fenced MSVC corner, which covers the deprecated experimental coroutine headers and the deprecated /await switch in Visual Studio 2026. Everything else in the guide is GCC. MSVC was not tested; no MSVC result is claimed.

Where do C++20 modules fit in the new default?

Modules are still experimental in GCC 16 and must be enabled with -fmodules, according to the changes page. The new default does not make modules work without that flag. See the course guide on C++20 Modules with CMake 3.28 and Ninja.

Source

  • GCC, GCC 16 Release Series — release history: GCC 16.1 on 30 April 2026, GCC 16.2 on 7 August 2026; re-checked 4 November 2026, no later release listed.
  • GCC, GCC 16 Changes — the C++20-by-default passage; the json diagnostics format removal and SARIF direction; C++20 no longer experimental; modules still experimental under -fmodules; the C++20 ABI-change list and the std::variant macro; hierarchical diagnostics.
  • GCC, Porting to GCC 16 — the -Wunused-but-set-* level change; the operator!= ambiguity warning and example error; the operator>> into char* removal; the Autoconf before 2.73 -std=gnu++11 trap; concept and requires as identifiers; [=] this capture deprecation; char8_t literal type change; missing-header reliance; removed std::allocator members.
  • Microsoft Learn, C++ features deprecated or removed from Visual Studio — Visual Studio 2026 deprecation of the experimental coroutine headers and of the /await switch; MSVC corner only.

Facts verified 2026-11-04. Every code listing on this page was compiled and run with GCC 16.2.0 at its default dialect on Ubuntu 24.04, including the programs that fail: the error messages are quoted from the actual compiler output, and the failing programs are labeled as failing. The same programs were attempted with GCC 14.2.0 and GCC 13.3.0 at their defaults and with GCC 16.2.0 plus -std=gnu++17, with the results in the compiler matrix. GCC 15, MSVC, and Clang with libc++ were not tested. The GCC 16.2.0 compiler on the build box was obtained by downloading Ubuntu's own g++-16 packages for a newer Ubuntu release and extracting them into a user directory, without installing them system-wide; the compiler is the stock Ubuntu 16.2.0 build. The operator!= ambiguity error and the Autoconf error are quoted from the GCC porting page, labeled as such, because the ordinary operator!= forms tried on the build box compiled.

Keep Exploring

Keep exploring the docs, samples, and free tools to continue your evaluation.