GCC 16 finished C23. Now wait for your distro to catch up.
Published by RodHat

C11 shipped in 2011. C17 was a typo-and-clarification release in 2018, barely worth the version bump. The real next C was C23, and it took until October 2024 to get the ISO publication out the door. GCC 16 landed in April 2026 with the complete implementation. It is now August. Your distro may or may not have GCC 16 yet. This is where we live.
I am going to say something I don’t say often: C23 is actually good. Not flashy, not trying to be Rust-with-manual-memory, not bolting on a type system through squinting. It is C making corrections that were overdue for a decade and adding features that systems programmers have wanted since roughly forever. GCC 16 is the first compiler to implement all of them, and if you write C for a living you should know what changed.
typeof is finally a keyword
C11 had __typeof__. That’s the GCC extension spelling. You could use typeof as
an identifier in C11 code and nothing would break, which is the polite way of saying
the standard didn’t own the name. C23 fixes that. typeof(expr) is now a proper
keyword, returns the type of an expression without evaluating it, and you can use it
to write macros that don’t lie about their operand types:
#define MAX(a, b) \
({ typeof(a) _a = (a); typeof(b) _b = (b); _a > _b ? _a : _b; })
That’s the GCC statement-expression extension combined with typeof. The typeof
part is now portable C23. The statement-expression part is still a GCC extension;
you don’t get everything at once. The point is that typeof in C23 code is no
longer secretly relying on a GNU extension; it’s in the standard. Write it once,
it compiles correctly on any C23 compiler. This matters when Clang 20 or MSVC
eventually catches up.
typeof_unqual also exists in C23, same as typeof but strips qualifiers.
Useful when you want the base type without dragging const or volatile along.
#embed: no more xxd pipe tricks
This one I have been waiting for since roughly 2003. #embed reads a file at
compile time and embeds its contents as a sequence of integer constants:
const unsigned char shader_src[] = {
#embed "shaders/vertex.glsl"
};
Before C23, embedding binary data at compile time required either a build system
step (xxd -i input.bin > input.h), a linker script (SECTIONS { .mydata : { *(.mydata) } }), or accepting that you’d open the file at runtime and handle the
error path. The xxd approach works but it generates a file that’s not tracked in
version control, or is tracked and constantly dirty, or you write a Makefile rule
and pray nobody runs the build out-of-order.
#embed handles files up to the compiler’s translation limit. It accepts a limit
parameter to cap how many bytes you pull in, a prefix and suffix for wrapping
syntax, and falls back gracefully with if_empty if the file has zero bytes. It is
cleaner than every workaround that existed before it.
GCC 16 implements it. This is one of those features where the five-year argument about the proposal is completely forgotten the moment you use it once.
_BitInt(N): explicit-width integers without the preprocessor
_BitInt(128) gives you a 128-bit integer. _BitInt(7) gives you a 7-bit integer.
The signed variant is _BitInt(N), unsigned is unsigned _BitInt(N). The width
can be any positive integer up to the implementation’s limit.
For systems programming this matters in two places: cryptography, where intermediate
values at non-standard widths avoid truncation bugs; and bitfield-dense protocol
parsing, where you’re shuffling around fields that don’t align to 8/16/32/64
boundaries. Before this you were using platform-specific extensions or __uint128_t
for the 128-bit case and writing manual masks for everything else. The stdbit.h
header in C23 also adds a full set of bit-counting and rotation operations
(stdc_bit_width, stdc_count_ones, stdc_rotate_left, etc.) that are properly
typed and work on _BitInt values.
The hardware support varies. On x86-64 with GCC 16, wide _BitInt operations
synthesize to multi-word sequences; the compiler handles it, you get no new
machine instructions. On architectures with native 128-bit operations you’ll see
them. Don’t write code that assumes one behavior; let the compiler pick.
nullptr, bool, true, false: no more <stdbool.h>
nullptr is a keyword now. It has type nullptr_t, which is distinct from all
pointer types and from integer types, which means a function that takes nullptr_t
can’t accidentally accept a zero integer or a void pointer without an explicit cast.
NULL still exists for compatibility. Use nullptr in new code.
bool, true, and false are keywords, not macros. <stdbool.h> still exists
and is still a valid include, but its contents are now just documentation; the
keywords exist without the header. This means code that accidentally used bool as
an identifier in C11 (where it was technically legal if you didn’t include
<stdbool.h>) will break under -std=c23. That’s correct behavior. bool should
have been a keyword in 1989 and anyone who used it as a variable name was already on
thin ice.
constexpr and [[attributes]]
constexpr in C23 is not constexpr in C++; it doesn’t apply to functions, only
to objects. A constexpr object is guaranteed to be a constant expression, which
means it can appear in places that require one (array dimensions, bit-field widths,
_BitInt widths). It replaces the enum { BUFFER_SIZE = 4096 } hack for defining
integer constants that are actually typed and not just int.
The [[]] attribute syntax ([[deprecated]], [[nodiscard]], [[noreturn]],
[[maybe_unused]]) is now standard C. If you’ve been writing __attribute__((...))
for GCC and __declspec(...) for MSVC and wrapping them in a preprocessor maze of
macros, that’s over for the attributes the standard absorbed. New compiler-specific
attributes still need the old syntax. This is progress, not a complete solution.
%b in printf format strings is also new: it prints an integer in binary. This is
the one C23 feature that anyone can explain in one sentence and everyone who has
ever written a bitmask debugger will immediately use.
The distribution situation
GCC 16 is in Arch Linux and Gentoo unstable now, obviously. Debian 13 Trixie ships
GCC 14 as the default and makes 16 available in gcc-16; a full default switch is a
Debian 14 conversation. Ubuntu 26.04 LTS defaulted to GCC 15 at release with 16 in
the toolchain PPA. On RHEL-derivatives, the RHEL 10 toolchain landed on GCC 15 and
the next-version bump is not imminent.
In practice: if you’re on a rolling distribution you can use -std=c23 against GCC
16 today. If you’re on a stable distribution you’re more likely running GCC 15, which
has most of C23 but not all of it. _BitInt support in GCC 15 is partial; #embed
was not in GCC 15. Before you mark a codebase as C23-required, check what your CI
runner has.
The one thing I’ll give this: the gap between spec publication (October 2024) and complete compiler implementation (April 2026) is about 18 months. For an ISO standard, that’s fast. C11 took GCC several years to implement completely. The WG14 process and the GCC development cycle clearly got less decoupled than they used to be, probably because the same humans participate in both. That’s the correct organizational decision.
Fourteen years between C11 and a standard worth using. I’ll take it.
See also: POSIX 2024 and what actually changed in make for the adjacent story of a different standards body finally shipping an update that had been pending for years, and Rust in the Linux kernel isn’t failing for the broader context of why systems programmers are paying attention to language-level progress again.
Sources
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Compiler Collection)
- N3220, Working Draft, Programming Languages, C (final pre-publication draft) (ISO/IEC JTC1/SC22/WG14)