What's New in C23

C23, published in 2024, is the biggest update to C in decades. It modernises everyday code — bool as a real keyword, nullptr, constexpr, typeof, digit separators — and adds features such as #embed and standard attributes. This lesson tours the most useful additions.

Using C23

Compile with -std=c23 (older compilers call it -std=c2x). GCC 14+ and Clang 18+ support most of it; GCC 15 makes C23 its default. Every example in this lesson was compiled with -std=c23.

$
gcc -std=c23 -Wall -Wextra main.c -o main

bool, true and false are keywords

No more #include <stdbool.h>: bool, true and false are built in.

C
#include <stdio.h>

bool is_even(int n) { return n % 2 == 0; }

int main(void) {
    bool ready = true;
    printf("%d %d %d\n", ready, is_even(4), is_even(7));
    return 0;
}
Output
1 1 0

nullptr

nullptr is a null pointer constant with its own type (nullptr_t), so it can't be confused with the integer 0 the way NULL sometimes can:

C
#include <stdio.h>
#include <string.h>

int main(void) {
    const char *at = strchr("no-at-sign", '@');
    if (at == nullptr) {
        printf("not an email address\n");
    }
    return 0;
}
Output
not an email address

constexpr

constexpr objects are true compile-time constants with a type — usable anywhere a constant expression is needed, such as array sizes at file scope. It replaces many #defines:

C
#include <stdio.h>

constexpr int MAX_PLAYERS = 4;
constexpr double TAX_RATE = 0.2;

int scores[MAX_PLAYERS];

int main(void) {
    printf("%zu slots, tax %.0f%%\n", sizeof scores / sizeof scores[0], TAX_RATE * 100);
    return 0;
}
Output
4 slots, tax 20%

typeof and auto

typeof(expr) gives the type of an expression — useful in macros. auto declares a variable whose type is deduced from its initialiser:

C
#include <stdio.h>

#define SWAP(a, b) do { typeof(a) tmp_ = (a); (a) = (b); (b) = tmp_; } while (0)

int main(void) {
    double x = 1.5, y = 9.25;
    SWAP(x, y);
    printf("%.2f %.2f\n", x, y);

    auto count = 3;          // int
    auto ratio = 0.75;       // double
    printf("%zu %zu\n", sizeof count, sizeof ratio);
    return 0;
}
Output
9.25 1.50
4 8

Number literals

Binary literals are now standard, and a ' can separate digit groups for readability:

C
#include <stdio.h>

int main(void) {
    long population = 8'200'000'000;
    unsigned mask = 0b1111'0000;
    printf("%ld %u\n", population, mask);
    return 0;
}
Output
8200000000 240

Attributes

Standard attributes in [[ ]] tell the compiler your intent so it can warn you:

AttributeMeaning
[[nodiscard]]Warn if the caller ignores the return value
[[maybe_unused]]Don't warn if this is unused
[[deprecated("reason")]]Warn when it's used
[[fallthrough]]Intentional fall-through in a switch
[[noreturn]]The function never returns (like exit)
C
#include <stdio.h>
#include <stdlib.h>

[[nodiscard]] int *make_buffer(size_t n) {
    return malloc(n * sizeof(int));
}

[[deprecated("use make_buffer")]] int *old_buffer(void) {
    return malloc(16);
}

int main(void) {
    make_buffer(10);            // result ignored: leak!
    int *p = old_buffer();
    free(p);
    return 0;
}
Compiler warnings
main.c:13:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
main.c:14:14: warning: 'old_buffer' is deprecated: use make_buffer [-Wdeprecated-declarations]

Smaller conveniences

C
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

enum small : unsigned char { LOW, MID, HIGH };     // choose an enum's underlying type

int main(void) {
    int counts[5] = {};                            // empty initialiser: all zero
    static_assert(sizeof(int) >= 4);               // message now optional
    char *copy = strdup("standard at last");       // strdup is now in the standard
    printf("%d %zu %s\n", counts[4], sizeof(enum small), copy);
    free(copy);
    return 0;
}
Output
0 1 standard at last

C23 also makes () in a function declaration mean "no parameters", exactly like (void), removes old-style (K&R) function definitions, and requires two's-complement representation for signed integers — though signed overflow is still undefined.

#embed

#embed includes the bytes of any file directly into an array at compile time — icons, fonts, shaders, test data — without external tools:

TXT logo.txt
<C>
C
#include <stdio.h>

static const unsigned char logo[] = {
#embed "logo.txt"
};

int main(void) {
    printf("embedded %zu bytes: ", sizeof logo);
    for (size_t i = 0; i < sizeof logo; i++) {
        printf("%c", logo[i]);
    }
    return 0;
}
Output
embedded 4 bytes: <C>

Library additions

  • <stdbit.h> — bit utilities such as stdc_count_ones and stdc_bit_width (not yet in every C library; macOS lacks it at the time of writing).
  • <stdckdint.h> — checked arithmetic: ckd_add(&result, a, b) returns true if the result overflowed.
  • memset_explicit — clears memory (such as passwords) in a way the optimiser won't remove.
  • unreachable() in <stddef.h> — marks code that can never run.

Should you use C23?

For new projects with modern compilers, yes — the code is clearer and safer. If your code must build with older compilers, embedded toolchains or MSVC (which supports only parts of it), stay with C17 for now and adopt C23 features as your tools catch up.

Try it yourself

Rewrite an earlier program of yours using constexpr, nullptr, bool without a header, and digit separators.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c23 -Wall main.c -o main && ./main. How to set up a C compiler
Output
Perfect numbers below 1000: 6 28 496