April 25, 2025·6 min read·engineeringcgccc23toolchains

GCC 15 makes GNU C23 the default language

The default C dialect changes in GCC 15.1. Empty parameter lists, keyword conflicts, and binary resource inclusion explain the practical consequences.

A compiler upgrade can change the language of a build whose source files and command line stay the same. GCC 15.1 does exactly that for C. Without an explicit language selection, compilation now uses -std=gnu23 instead of -std=gnu17. The April 25 release therefore makes C23 a practical compatibility question for projects that have left their dialect implicit.

The change is GNU C23, including GNU extensions. It is not the same as selecting strict ISO C23 with -std=c23. That distinction belongs in any explanation of the new default because it determines what a successful build establishes. Code can compile under the default while depending on extensions outside the published language standard.

I retain considerable affection for C, but its apparent familiarity makes implicit build assumptions easy to overlook. A language small enough to understand still has multiple revisions, and those revisions can assign different meanings to identical declarations. GCC 15 makes that fact visible in old interfaces that previously compiled without argument information.

An empty parameter list now makes a promise

The GCC 15 porting guide identifies function declarations without parameters as a concrete source of failures. In C17 and earlier, a declaration such as int f(); supplied no information about the number or types of parameters. Calling code still needed the correct arguments, with the default argument promotions applied. That spelling did not declare that the function accepted no arguments.

In C23, the declaration means a function taking no parameters, equivalent in that respect to int f(void);. A build that changes dialect can therefore discover incompatible function pointers or calls with arguments even though the declaration text has not changed. The compiler has acquired a stricter promise from the same tokens.

The porting guide illustrates the issue with a signal-handler pointer. A declaration with empty parentheses can no longer stand in for the pointer to a function accepting an int. Stating the parameter type gives the interface the information that it needed all along.

void (*handler)(int);

There is another common source of this spelling. A source file may have declared a library function manually instead of including its header. The guide gives void *malloc(); as an example. Under the new interpretation, that declaration says the function takes no arguments. Including the correct standard header and removing the incomplete declaration repairs the interface description.

These cases explain why an upgrade can produce an error that looks like a new compiler objection to old working code. The previous declaration withheld information. C23 gives the empty list a specific meaning. The useful repair is usually an accurate declaration, though a project can explicitly select an older standard while it manages the transition.

New keywords can change an interface migration

C23 adds keywords including bool, true, false, nullptr, and thread_local. A project that previously used one of those spellings as an identifier must account for its new grammatical role. GCC's porting guide demonstrates a failure for typedef int bool;, which conflicts with the C23 keyword.

Renaming or removing the typedef is not necessarily the entire change. The guide warns that bool and int differ at the ABI level. If the old name appeared in declarations crossing a binary interface, substituting the language Boolean type can change the interface being described. Serialized data deserves the same attention when its representation depends on the old integer type.

The guide recommends stdbool.h for compatible definitions in C99 and later. That addresses source spelling across the language versions it describes. It does not establish that a historical project-specific integer typedef had the same representation as the standard Boolean type. The distinction between a familiar name and its actual type matters most at those boundaries.

For a build that deliberately remains on the previous GNU dialect, the selection is explicit. This command illustrates that choice for a source file named source.c.

gcc -std=gnu17 -c source.c

An explicit flag gives the build a documented language choice. It also separates a compiler upgrade from a language migration. That separation can be useful for maintaining existing software, while code that adopts C23 can use -std=c23 or -std=gnu23 according to its extension requirements.

Binary data becomes a preprocessor input

GCC 15 also implements the C23 #embed directive. The GCC 15.1 preprocessor manual defines it as efficient inclusion of binary data, commonly in array initializers. GCC supports it in C23 mode and as an extension in older C modes and C++.

The directive expands as a comma-separated sequence of integer literals representing the resource bytes. It performs no character-set conversion. That makes the semantics different from inserting a source file containing C tokens with #include. A binary resource supplies data for the initializer rather than source syntax written by a separate generator.

For a nonempty file named payload.bin beside the source file, an array can take its contents directly. The example assumes the file is present during preprocessing.

static const unsigned char payload[] = {
#embed "payload.bin"
};

The manual specifies the search behavior. Quoted names first search the directory of the source file using the directive, then directories named by --embed-dir=. The angle-bracket form uses the configured embed directories for a relative name. These rules make the resource location part of the build input configuration, with a dedicated search option.

Optional parameters let the source limit the number of included bytes or add tokens around the expansion. The standard parameters described by GCC are limit, prefix, suffix, and if_empty. The last supplies a replacement token sequence when the resource is empty. That is useful when the surrounding initializer needs a deliberate empty-resource representation instead of inheriting one accidentally.

This facility makes the relationship between a resource and its consuming declaration visible in the C source. It does not remove the resource from the build dependencies. The compiler still needs the file at preprocessing time, and its contents determine the resulting initializer. The improvement is that the language now describes that inclusion without requiring the resource to be rendered as handwritten C data.

Initialization has a separate compatibility change

The release summary also identifies a union-initialization change that deserves separate treatment. For a union without static storage duration, {0} no longer guarantees clearing the whole union. It initializes the first member to zero. Code that depended on the broader clearing behavior was depending on more than that member initialization expressed.

GCC documents {} as the spelling to use when initialization of the whole union, including padding bits, is desired. That form is valid in C23 and C++. The release also provides -fzero-init-padding-bits=unions to restore the older GCC behavior. These are distinct choices, one in the source and another in the compiler configuration.

This issue is independent of whether an empty parameter list still compiles. A project can finish correcting declarations and still have representation assumptions in initialization code. The release notes explicitly exempt static-storage initialization from the described change, so a blanket statement about every union initializer would be inaccurate.

The distinction is particularly relevant when reading code that treats an initialized union as a complete byte representation. A zero value in one member is a statement about that member. It does not by itself describe every byte occupied by the union under the changed GCC behavior. The documented source spelling allows the program to state the broader initialization requirement when that is what its surrounding logic needs.

The next draft remains a separate mode

The GCC 15 change summary lists experimental support for selected C2y work under -std=c2y and -std=gnu2y. It also notes that some of those features are available as extensions in older modes. Their presence in the compiler does not change the identity of the default dialect, which is GNU C23.

GCC updates __STDC_VERSION__ to 202311L in its C23 modes. The same release adds the unsequenced and reproducible attributes as further C23 implementation work. These are additions within the selected language version, alongside the default change that determines how existing code is interpreted.

I regard explicit language selection as part of a C interface's maintenance. The concrete reason in this release is the empty parameter list. Keeping f() in a declaration while changing dialect changes the information that declaration conveys. Recording the intended dialect and writing the actual parameter types makes that decision visible both to the compiler and to the next person reading the header.