May 6, 2022·6 min read·engineeringcppgcccpp23

GCC 12 adds multidimensional subscripts and broader constexpr bodies

GCC 12 implements C++23 changes that separate indexing from function calls and permit runtime-only code inside constexpr functions. The new syntax keeps array layout and constant-evaluation validity as separate responsibilities.

A multidimensional array library should not need to look callable merely because C++ limits subscript operators to one argument. A constexpr function should not need a separate helper merely because one runtime branch uses a type forbidden in its body. GCC 12 implements C++23 changes that address both restrictions, along with if consteval for choosing an evaluation-specific path.

The GCC 12 series begins with the May 6 release of GCC 12.1. Its C++ changes identify P2128R6 for multidimensional subscripts and P2242R3 for non-literal variables, labels, and gotos in constexpr functions. These are separate language changes with related benefits for library authors.

I consider C++'s ability to express abstractions without compulsory runtime overhead one of its strengths. The difficulty is often the amount of language machinery needed to state the abstraction. These additions are interesting because they remove specific workarounds while retaining the programmer's responsibility for indexing and object lifetime.

Indexing can take all coordinates at once

P2128R6 permits a user-defined subscript operator to accept multiple arguments. A matrix-like object can therefore use matrix[row, column] without first constructing an intermediate row object. The proposal also allows zero arguments and variadic argument lists.

The following small class illustrates the two-argument form. Its element access assumes the caller supplies coordinates within the declared extent. The bounds policy is intentionally separate from demonstrating the new syntax.

#include <cstddef>

struct matrix_view {
    double *data;
    std::size_t columns;

    double& operator[](std::size_t row, std::size_t column) const
    {
        return data[row * columns + column];
    }
};

void set_entry(matrix_view matrix)
{
    matrix[1, 2] = 4.0;
}

The example chooses a contiguous row-major mapping. The language change does not impose that layout on other types. A class can map the coordinates according to its own storage rules and return a reference or an appropriate reference-like object. The square-bracket syntax describes the indexing interface, while the implementation defines how indices identify storage.

The proposal explicitly leaves built-in C arrays unchanged. It does not reinterpret an array-of-arrays declaration as a new multidimensional object with a multi-argument builtin operator. Existing nested indexing remains available. Applying the new form to an application class and changing built-in array semantics would be different language changes.

Chained indexing can imply an unnecessary object

The proposal compares several workarounds. One uses operator() with all coordinates. Another chains individual subscript operations. A third passes an aggregate containing coordinates to a single-argument subscript. Each can express access, but each also introduces assumptions or machinery beyond the operation the programmer intended.

In a chain such as a[x][y], the first access must produce something the second access can index. For some layouts, representing an intermediate row is natural. For others, such as the sparse representations discussed in the paper, constructing that view can require work unrelated to obtaining one element.

The intermediate object also complicates generic code. Saving a[x] into an automatic variable may copy an owning object for one container and copy a cheap proxy for another. Saving a reference has different lifetime implications if the operation returns a temporary proxy. A single multi-argument call avoids requiring that intermediate abstraction merely to express one access.

Using parentheses has another consequence. A type that overloads the call operator for indexing can appear callable to generic machinery. P2128R6 discusses interfaces that take a function and arrays, where accidentally exchanging those arguments can then be harder to diagnose. Giving indexing its own operator preserves a useful distinction in the interface.

These are design arguments from the proposal, not a promise of a speedup on every matrix expression. The authors explicitly discuss difficulty demonstrating inlining problems in small examples. The reliable language-level result is that an array class can receive all coordinates in a single subscript operation.

Existing comma syntax requires care

The commas in the new subscript argument list have a defined role. The proposal explains that unparenthesized comma expressions in subscript expressions were deprecated in C++20. Its new model makes the previously deprecated form ill-formed where there is no appropriate new interpretation, rather than silently giving built-in arrays multidimensional semantics.

That history matters when reading older source. An expression that previously used a comma operator inside square brackets did not necessarily index two dimensions. A source migration must determine the expression's intended meaning before replacing punctuation or adding an overload. The new spelling is useful because the type can define a genuine multi-argument operation.

P2128R6 also declines to rewrite a[x][y] automatically into a multi-argument call. Such rewriting would make a familiar subexpression's meaning depend on additional overload resolution for the complete chain. The proposal discusses those ambiguities and keeps its change focused on the new argument-list form.

A constexpr body can contain a runtime path

P2242R3 addresses restrictions on what may appear in a constexpr function's body. The important distinction is between permitting a construct in the function and permitting its execution during constant evaluation. A function may offer both constant-evaluation and runtime uses without every statement being valid on both paths.

The change allows non-literal variables and other formerly prohibited constructs to appear in the body. It does not make execution through those constructs a valid core constant expression. This lets a function place runtime-only work directly in the appropriate branch instead of extracting it into a separate non-constexpr helper solely to satisfy a body-wide restriction.

The paper's rationale concerns composition and maintenance. Duplicating a function or splitting off a helper can obscure the relationship between its runtime and compile-time implementations. Relaxing the body restriction permits that relationship to remain visible while constant evaluation still rejects a path that actually performs an impermissible operation.

I prefer that distinction when a constexpr function has a legitimate runtime implementation. It keeps the relevant branch in the same function. The guarantee remains conditional on the executed path, which is a more precise statement than treating every token inside a constexpr body as a promise of compile-time executability.

P2242R3 makes the static-storage case explicit. Control flow that passes through a declaration of a static or thread-local variable prevents the expression from qualifying as a core constant expression. A branch that avoids that declaration can still be useful for constant evaluation. The distinction concerns passage through the declaration, not merely whether its initializer looks simple.

The proposal also raises __cpp_constexpr to 202103L. Its rationale notes that avoiding separate helper functions can reduce overload-resolution and template-instantiation work. Feature detection therefore helps code select a simpler source implementation when the new body rules are available, without making an unsupported promise about evaluation of those runtime-only declarations.

if consteval supplies the context boundary

GCC 12 also implements P1938R3. The new if consteval statement selects its first block during manifest constant evaluation and its alternative block at runtime. Its constant-evaluation block is an immediate function context, which permits certain calls to consteval functions that an ordinary boolean test does not make valid.

This complements the body relaxation. One change permits runtime-only constructs to appear in a constexpr function. The other expresses a branch whose context allows immediate-function calls. Neither establishes that every operation in the other branch can run during constant evaluation.

The distinction also avoids confusing constant evaluation with ordinary compiler optimization. A runtime call may be optimized aggressively without becoming the kind of evaluation selected by the language's consteval statement. These rules concern program semantics, so the chosen branch is not simply a report of whether the optimizer folded a calculation.

Compiler syntax and library availability remain separate

The GCC notes also describe experimental C++23 library work, including std::expected and std::move_only_function. Those additions come from the accompanying standard library. Support for multi-argument subscript syntax does not prove that a particular deployment has every library type that could use the syntax.

An application-defined view such as the example needs the language facility and the declarations it uses. A program adopting a proposed standard multidimensional view additionally needs that library implementation. Naming the relevant requirements separately makes it possible to adopt a compiler improvement without assuming completion of the entire revision.

GCC 12's language additions consequently give library authors more direct ways to express operations they already implement. A subscript can take its full coordinate set, and a constexpr function can retain runtime-specific code in its body. The remaining contracts still belong to the type and function, including storage mapping, valid coordinates, and the operations actually executed during constant evaluation.