C Standards and C23
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C is one of the few languages whose 1989 standard still describes code that compiles today. Each edition since has been additive and conservative, which is why "C" means something stable — and why knowing which edition your toolchain implements matters more than in a faster-moving language.
The Timeline
The naming confusion is worth clearing up once: C17 and C18 are the same document (published in 2018, targeting 2017), and C23 was published in 2024 — so ISO/IEC 9899:2024 is "C23". The committee’s own working draft for it is N3220, and WG14 states it differs from the published standard only editorially, which is why these pages cite it: the ISO text itself is a paid document.
Detecting the Edition
__STDC_VERSION__ is the only reliable test:
#include <stdio.h>
int main(void)
{
#if !defined(__STDC_VERSION__)
const char *edition = "C89/C90"; // the macro did not exist yet
#elif __STDC_VERSION__ >= 202311L
const char *edition = "C23";
#elif __STDC_VERSION__ >= 201710L
const char *edition = "C17";
#elif __STDC_VERSION__ >= 201112L
const char *edition = "C11";
#elif __STDC_VERSION__ >= 199901L
const char *edition = "C99";
#elif __STDC_VERSION__ >= 199409L
const char *edition = "C95";
#else
const char *edition = "unknown";
#endif
printf("compiling as %s (__STDC_VERSION__ = %ldL)\n", edition, __STDC_VERSION__);
return 0;
}
| Edition | __STDC_VERSION__ |
|---|---|
C89/C90 |
not defined |
C95 |
|
C99 |
|
C11 |
|
C17/C18 |
|
C23 |
|
What C23 Added
Keywords That Were Macros
#include <stdio.h>
int main(void)
{
// All four are KEYWORDS in C23; <stdbool.h> is no longer needed.
bool ready = true;
bool done = false;
// static_assert and thread_local likewise -- no <assert.h> macro,
// no _Static_assert/_Thread_local spelling required.
static_assert(sizeof(int) >= 2, "int must be at least 16 bits");
static_assert(sizeof(int) >= 2); // C23: the message is optional
printf("%d %d\n", (int)ready, (int)done);
return 0;
}
The C11 spellings (_Bool, _Static_assert, _Thread_local, _Alignas, _Alignof, _Noreturn) all still
work, which is what lets one header serve both editions. <stdbool.h>, <stdalign.h> and <stdnoreturn.h>
still exist but are deprecated.
nullptr
#include <stddef.h>
#include <stdio.h>
static void takes_pointer(void *p)
{
printf("%s\n", p == nullptr ? "null" : "not null");
}
int main(void)
{
int *p = nullptr; // type nullptr_t, converts to any pointer type
takes_pointer(nullptr);
takes_pointer(&p);
// Why it matters: NULL may expand to plain 0, so in a VARIADIC call it can
// pass an int where a pointer is expected. nullptr always passes a pointer.
printf("%d\n", p == NULL);
return 0;
}
constexpr
#include <stdio.h>
constexpr int MAX_ITEMS = 64; // typed, scoped, a constant expression
constexpr double TAU = 6.283185307179586;
constexpr unsigned char MASK = 0xF0;
int main(void)
{
int items[MAX_ITEMS]; // not a VLA -- a real constant
printf("%zu %g %u\n", sizeof items / sizeof items[0], TAU, MASK);
return 0;
}
Unlike C++, C’s constexpr applies to objects only, never functions. See
Constants, Enumerations and
Initialization.
typeof and auto
#include <stdio.h>
int main(void)
{
int value = 42;
typeof(value) same_type = value; // int
typeof(&value) pointer = &value; // int *
const int frozen = 7;
typeof_unqual(frozen) writable = frozen; // int, const stripped
writable = 8;
auto inferred = 3.5; // double, from the initializer
auto text = "hello"; // char *
printf("%d %d %d %g %s\n", same_type, *pointer, writable, inferred, text);
return 0;
}
_BitInt(N) — Bit-Precise Integers
#include <limits.h>
#include <stdio.h>
int main(void)
{
_BitInt(12) twelve_bits = 2047;
unsigned _BitInt(3) three_bits = 7;
_BitInt(128) very_wide = 170141183460469231731687303715884105727wb;
printf("%d %u %d (max width %d)\n",
(int)twelve_bits, (unsigned)three_bits,
(int)(very_wide >> 120), (int)BITINT_MAXWIDTH);
return 0;
}
Literals and Initializers
#include <stdio.h>
struct Config { int a; int b; char name[8]; };
int main(void)
{
int binary = 0b1010'1010; // binary literal + digit separators
long long big = 1'000'000'000'000LL;
unsigned char byte = 0b1111'0000;
struct Config empty = { }; // empty initializer: everything zeroed
int zeros[4] = { };
printf("%d %lld %u %d %d\n", binary, big, byte, empty.a, zeros[3]);
return 0;
}
Attributes
#include <stdio.h>
[[nodiscard("the status must be checked")]] static int operation(void)
{
return 0;
}
// External linkage and defined below, so it is not referenced here -- calling it
// is what produces "warning: 'old_operation' is deprecated: use operation() instead".
[[deprecated("use operation() instead")]] int old_operation(void);
int old_operation(void)
{
return 1;
}
static int classify(int value, [[maybe_unused]] int debug)
{
switch (value) {
case 1:
[[fallthrough]];
case 2:
return 10;
default:
return 0;
}
}
int main(void)
{
// __has_c_attribute lets a header probe before using one:
#if defined(__has_c_attribute) && __has_c_attribute(nodiscard)
puts("nodiscard is available");
#endif
printf("%d %d\n", operation(), classify(1, 0));
return 0;
}
The standard set: [[deprecated]], [[fallthrough]], [[maybe_unused]], [[nodiscard]], [[noreturn]],
[[unsequenced]] and [[reproducible]].
Preprocessor Additions
#include <stdio.h>
// __VA_OPT__ makes a zero-argument variadic macro standard:
#define LOG(format, ...) printf("[log] " format "\n" __VA_OPT__(,) __VA_ARGS__)
int main(void)
{
LOG("no arguments"); // works in C23, was a GNU extension before
LOG("value = %d", 42);
// #elifdef / #elifndef shorthands:
#if 0
puts("never");
#elifdef __STDC_VERSION__
puts("elifdef works");
#endif
// __has_include, standardized:
#if defined(__has_include) && __has_include(<stdbit.h>)
puts("<stdbit.h> is present");
#endif
return 0;
}
#embed also arrives in C23 — see
Preprocessor and Macros.
New Library Headers and Functions
| Addition | What it gives you |
|---|---|
|
|
|
|
|
A |
|
Standardized at last (POSIX had them for decades). |
|
Deallocation that takes the size back, letting allocators skip bookkeeping. |
|
UTC conversion and the reentrant time functions. |
|
In |
|
In |
|
Binary output, matching the new binary literals. |
What C23 Removed or Deprecated
-
Trigraphs (
??=,??(, …) — removed outright. -
K&R function definitions and unprototyped declarations — removed.
void f()now meansvoid f(void). -
Implicit
intand implicit function declarations — already gone in C99, now firmly errors. -
gets— removed in C11, still worth naming. -
Deprecated:
<stdbool.h>,<stdalign.h>,<stdnoreturn.h>,<iso646.h>,asctime,ctime, and the_Noreturnspelling.
The removals are the reason a very old codebase may fail under -std=c23; nothing that compiled cleanly with
-Wall -Wextra under C17 is affected.
Compiler Support
As of 2026, and worth checking against your own toolchain rather than trusting a table:
| Feature | GCC | Clang | MSVC |
|---|---|---|---|
|
14+ (13 uses |
18+ (17 uses |
|
|
13+ |
15+ |
partial |
|
13+ |
16+ |
no |
|
13+ (extension since forever) |
16+ |
no |
|
13+ |
16+ |
no |
|
14+ |
15+ |
no |
Binary literals and digit separators |
13+ |
15+ |
partial |
|
13+ |
15+ |
partial |
|
12+ |
12+ |
partial |
Empty initializer |
13+ |
16+ |
no |
|
14+ |
18+ |
no |
|
15+ |
19+ |
no |
|
15+ |
19+ |
no |
The two rows in bold are the ones to watch: constexpr and #embed are the last significant C23 features to
land, and code using them will not build on a toolchain from 2024.
Writing Transitional Code
The practical approach: write C23 where you can, and provide a compatibility shim for anything older you must still support.
// compat.h -- one header that lets the rest of the code be written in C23 style.
#ifndef PROJECT_COMPAT_H
#define PROJECT_COMPAT_H
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 202311L
/* Pre-C23: bring the keywords in as macros. */
# include <stdbool.h> /* bool, true, false */
# include <stddef.h> /* NULL */
# ifndef nullptr
# define nullptr NULL
# endif
# ifndef static_assert
# define static_assert _Static_assert
# endif
# ifndef thread_local
# define thread_local _Thread_local
# endif
/* No constexpr before C23: an enum covers the integer cases. */
# define COMPAT_INT_CONSTANT(name, value) enum { name = (value) }
/* Attributes, guarded on availability. */
# if defined(__GNUC__)
# define COMPAT_NODISCARD __attribute__((warn_unused_result))
# define COMPAT_NORETURN __attribute__((noreturn))
# define COMPAT_UNUSED __attribute__((unused))
# define COMPAT_FALLTHROUGH __attribute__((fallthrough))
# else
# define COMPAT_NODISCARD
# define COMPAT_NORETURN
# define COMPAT_UNUSED
# define COMPAT_FALLTHROUGH
# endif
#else /* C23 or later */
# define COMPAT_INT_CONSTANT(name, value) constexpr int name = (value)
# define COMPAT_NODISCARD [[nodiscard]]
# define COMPAT_NORETURN [[noreturn]]
# define COMPAT_UNUSED [[maybe_unused]]
# define COMPAT_FALLTHROUGH [[fallthrough]]
#endif
/* Optional headers are probed, not assumed. */
#if defined(__has_include)
# if __has_include(<stdckdint.h>)
# include <stdckdint.h>
# define COMPAT_HAVE_CKDINT 1
# endif
# if __has_include(<stdbit.h>)
# include <stdbit.h>
# define COMPAT_HAVE_STDBIT 1
# endif
#endif
#ifndef COMPAT_HAVE_CKDINT
# define COMPAT_HAVE_CKDINT 0
#endif
#ifndef COMPAT_HAVE_STDBIT
# define COMPAT_HAVE_STDBIT 0
#endif
#endif /* PROJECT_COMPAT_H */
Using it, with a fallback for checked arithmetic:
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L && __has_include(<stdckdint.h>)
# include <stdckdint.h>
#endif
// Checked addition that works on C23, on GCC/Clang before it, and anywhere else.
static bool add_checked(int a, int b, int *out)
{
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L && __has_include(<stdckdint.h>)
return !ckd_add(out, a, b);
#elif defined(__GNUC__)
return !__builtin_add_overflow(a, b, out); // GCC 5+ / Clang 3.8+
#else
if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) {
return false;
}
*out = a + b;
return true;
#endif
}
int main(void)
{
int result = 0;
bool ok = add_checked(2, 3, &result); // write, then read -- never both in one expression
printf("2 + 3 : %d -> %d\n", ok, result);
printf("INT_MAX + 1 : %d\n", add_checked(INT_MAX, 1, &result));
return 0;
}
Guidance on choosing a baseline:
-
C17 is the safe default for code that must build anywhere, including MSVC and long-term-support distros.
-
C23 is a reasonable default for a new project on GCC 14+/Clang 18+ — provided you avoid
constexprand#embeduntil GCC 15/Clang 19 are your floor. -
Pin
-std=explicitly and build with both GCC and Clang; the default dialect differs between compilers and versions. -
Probe, do not assume:
__has_include,__has_c_attributeand__STDC_VERSION__are cheap, and acompat.hlike the one above is written once.
See Also
-
Getting Started — the editions table and the compilers.
-
Preprocessor and Macros — feature-test macros,
__VA_OPT__and#embed. -
Constants, Enumerations and Initialization —
constexprand the empty initializer. -
Type-Generic Programming —
typeof,autoand_Generic. -
C++: C++ Standards and C++26 — WG21’s release train, and where the two standards deliberately diverge.
References
-
WG14 N3220 — the C23 working draft (Annex M lists the changes in each edition).