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.
#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; }
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:
#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; }
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:
#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; }
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:
#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; }
9.25 1.50 4 8
Number literals
Binary literals are now standard, and a ' can separate digit groups for readability:
#include <stdio.h> int main(void) { long population = 8'200'000'000; unsigned mask = 0b1111'0000; printf("%ld %u\n", population, mask); return 0; }
8200000000 240
Attributes
Standard attributes in [[ ]] tell the compiler your intent so it can warn you:
| Attribute | Meaning |
|---|---|
[[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) |
#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; }
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
#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; }
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:
<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; }
embedded 4 bytes: <C>
Library additions
<stdbit.h>— bit utilities such asstdc_count_onesandstdc_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.
Perfect numbers below 1000: 6 28 496