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

timeline title C language editions 1978 : K and R C - The C Programming Language, 1st edition : no prototypes, implicit int, no void 1989 : ANSI C (C89) - X3.159-1989 : prototypes, void, const, volatile, the standard library 1990 : C90 - ISO/IEC 9899:1990 : the same language, adopted by ISO 1995 : C95 - Amendment 1 : wide characters, wchar.h, wctype.h, digraphs 1999 : C99 - ISO/IEC 9899:1999 : // comments, long long, stdint.h, VLAs, inline, restrict : designated initializers, compound literals, snprintf 2011 : C11 - ISO/IEC 9899:2011 : _Static_assert, _Generic, anonymous unions, alignas : threads.h, stdatomic.h, Unicode literals, gets removed 2018 : C17 / C18 - ISO/IEC 9899:2018 : no new features - defect fixes only 2024 : C23 - ISO/IEC 9899:2024 : bool/true/false, nullptr, constexpr, typeof, auto : _BitInt, binary literals, attributes, #embed, stdbit.h

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

199409L

C99

199901L

C11

201112L

C17/C18

201710L

C23

202311L

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

<stdckdint.h>

ckd_add, ckd_sub, ckd_mul — overflow-checked arithmetic.

<stdbit.h>

stdc_count_ones, stdc_bit_width, stdc_bit_ceil, endianness macros.

memset_explicit

A memset the optimizer may not remove — for wiping secrets.

strdup, strndup

Standardized at last (POSIX had them for decades).

free_sized, free_aligned_sized

Deallocation that takes the size back, letting allocators skip bookkeeping.

timegm, gmtime_r, localtime_r

UTC conversion and the reentrant time functions.

unreachable()

In <stddef.h>: marks a genuinely unreachable path.

nullptr_t

In <stddef.h>: the type of nullptr.

printf %b / %B

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 means void f(void).

  • Implicit int and 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 _Noreturn spelling.

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

-std=c23 flag accepted

14+ (13 uses c2x)

18+ (17 uses c2x)

/std:clatest

bool/true/false, static_assert, thread_local keywords

13+

15+

partial

nullptr

13+

16+

no

typeof / typeof_unqual

13+ (extension since forever)

16+

no

auto type inference

13+

16+

no

_BitInt(N)

14+

15+

no

Binary literals and digit separators

13+

15+

partial

[[attributes]]

13+

15+

partial

__VA_OPT__

12+

12+

partial

Empty initializer {}

13+

16+

no

<stdckdint.h>, <stdbit.h>

14+

18+

no

constexpr

15+

19+

no

#embed

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 constexpr and #embed until 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_attribute and __STDC_VERSION__ are cheap, and a compat.h like the one above is written once.

See Also