April 30, 2026·6 min read·engineeringcppgcccxx20reflection

GCC 16 changes the default before enabling reflection

GCC 16 defaults to GNU C++20 and adds experimental C++26 facilities. Language selection and library ABI changes affect ordinary upgrades before reflection enters the code.

A compiler upgrade can change the language of a source file even when nobody edits that file. GCC 16.1, released on April 30, changes the default C++ dialect from GNU C++17 to GNU C++20. The same release adds experimental C++26 reflection, expansion statements and contracts. These changes affect different groups of users. An existing build that omits a standard option receives a new dialect immediately, while reflection requires deliberate enabling.

The GCC 16 change notes make the selection explicit. The default moves from -std=gnu++17 to -std=gnu++20, and reflection uses -std=c++26 -freflection. A project can therefore encounter C++20 compatibility work before writing any reflection code. The release also marks the libstdc++ C++20 implementation as no longer experimental, with documented binary compatibility consequences for components that were experimental in earlier releases.

After a decade of C++ performance work, I accept that its abstractions can avoid runtime overhead. Compile times and diagnostics account for much of the cost. The harder maintenance problem is having several eras of the language in one codebase. GCC 16 makes that problem concrete by changing the default interpretation of existing code while providing another set of facilities for new code to explore.

The build command selects the language contract

An explicit standard option records which dialect a translation unit expects. These commands make different choices while using the same compiler release.

g++ -std=gnu++17 -c existing.cpp
g++ -std=gnu++20 -c existing.cpp
g++ -std=c++26 -freflection -c reflection.cpp

The first command retains the previous default dialect. Selecting the new default explicitly is the second choice. The third requests the experimental reflection support identified in the release notes. Treating those as separate configurations helps explain whether a compile failure comes from changed default language rules or newly enabled experimental syntax.

The distinction between a compiler release and a language mode also matters for dependencies. A library built under one mode does not automatically change mode because its consumer uses another. Build configuration must express the intended choices for each target. The release warning about porting code that relies on an older standard applies particularly to builds that previously delegated that decision to the compiler default.

C++20 modules remain experimental and require -fmodules, according to the change notes. Moving the default to C++20 therefore does not enable every feature associated with that standard. The support status of an individual facility remains relevant. A general statement that the compiler now defaults to C++20 cannot substitute for the specific option and implementation status of modules.

Reflection has its own implementation switch

The release identifies P2996R13 as the reflection implementation and lists associated proposals for annotations, base-class splicing, function-parameter reflection and error handling. It also implements the define_static facilities for strings, objects and arrays. This collection matters because reflection involves more than obtaining a description of a type. Code needs ways to use reflected information and handle cases where an attempted compile-time operation cannot succeed.

The versioned C++ option manual describes -freflection as enabling experimental C++26 reflection. That status belongs beside any example or build instruction. The switch makes a particular implementation available for experiments. It does not establish that another compiler, or a compiler invoked in its default mode, accepts the same code.

Expansion statements, identified as P1306R5 in the change notes, are another implemented C++26 facility. Their inclusion is relevant to compile-time programming, but support should still be described at the proposal level. Listing reflection and expansion statements together does not imply that all proposed metaprogramming facilities are implemented. GCC supplies a specific inventory of papers and, for some entries, explicit limitations.

One such limitation appears in constexpr structured bindings. GCC reports partial support for P2686R4. Structured bindings can be constexpr, but references to constexpr automatic variables remain unsupported. The release therefore contains examples of both broad new capability and incomplete implementation within a single feature family. A compiler version number alone is too coarse to describe that boundary.

Contracts expose evaluation policy through compiler options

GCC lists P2900R14 contracts among its C++26 additions. The option manual says -fcontracts enables support for the working-draft facility. It also documents evaluation policies, which determine what happens when a contract predicate fails. The presence of a contract in source does not by itself specify that every build performs the same runtime action.

Under the ignored policy, checks are omitted. The observed policy performs checks and invokes a violation handler, after which execution can continue when the handler returns. Enforce also invokes the handler but terminates execution after it returns. Quick enforce terminates immediately without calling the handler. These modes describe materially different failure behavior, not different levels of diagnostic verbosity.

The manual also describes constant-evaluation behavior. Observed checks participate in constexpr evaluation and produce a diagnostic on failure. Enforce makes a failing constexpr check ill-formed, and quick enforce has the same compile-time behavior because no runtime handler participates there. These distinctions are useful when comparing what a test establishes under one policy with the behavior of a deployed build under another.

Additional options control caller-side checks and definition-side checks. By default, caller-side checking is absent, while definition-side checking is enabled when contracts are active. The manual permits caller checking of preconditions alone or of both preconditions and postconditions. Check placement therefore forms part of the implementation configuration, alongside the chosen response to a violation.

This is where my preference for explicit resource and error behavior in C++ remains relevant. RAII gives scope exit a defined mechanism for cleanup under ordinary control flow. Contracts express a different part of the program, the conditions under which an operation is valid. Their usefulness depends on understanding the policy the implementation applies when a condition fails, particularly when that policy ends execution.

libstdc++ changes affect binary boundaries

The change notes identify ABI changes in several C++20 library components. Atomic waiting and notifying functions, semaphore types and synchronization used by syncstream are among them. The list also includes representations used by formatting, partial ordering and some range adaptors. GCC says this list is not necessarily complete because the earlier C++20 support was experimental.

The practical consequence is that a successful compile cannot establish binary compatibility with an older build using those components. Headers and object code participate in the same interface. Passing a type across a library boundary requires agreement about its representation and behavior, so rebuilding only the executable may leave a dependency on an incompatible earlier implementation.

GCC also documents a std::variant layout correction affecting a particular C++17 case. A class with a variant as its first member and a base matching an empty alternative with a nontrivial destructor receives a different member offset. The previous behavior can be restored with _GLIBCXX_USE_VARIANT_CXX17_OLD_ABI. This is a narrowly described compatibility option, not a general promise to recover every older library layout.

The separation between language mode and ABI matters here as well. Explicitly retaining C++17 can avoid the default dialect transition while still exposing library changes documented for that mode. Conversely, selecting C++20 says nothing by itself about whether all linked objects use compatible versions of components that were previously experimental.

Diagnostics and library behavior also change

GCC 16 gives various C++ diagnostics a hierarchical presentation using indentation and bullet points. The change notes offer -fno-diagnostics-show-nesting or -fdiagnostics-plain-output to restore the older presentation. Improved constraint diagnostics also explain why selected standard type traits are false. These changes address part of the compilation cost of using abstractions, the effort required to understand a failed instantiation.

On targets supporting 128-bit integers, libstdc++ also changes integral traits for __int128 to report true in strict dialects as well as GNU dialects. Template dispatch that depends on those traits can consequently select different code without a change to the type spelling.

The runtime library rewrites std::regex execution to use a heap-based stack instead of the system stack, avoiding stack overflow when matching larger strings. That change illustrates why an upgrade assessment extends beyond newly accepted syntax. Existing source can receive different runtime implementation behavior without adopting reflection, contracts or another C++26 facility.

GCC 16 consequently has several independently meaningful adoption points. An ordinary build receives a new default dialect, C++20 library users cross the documented end of experimental status, and explicit switches expose new C++26 work. Each point has a concrete compiler or library setting that can be recorded in the build, so failures can be discussed in terms of the configuration that produced them.