CFoundations

C23 Modern C: auto, nullptr, constexpr, typeof, Attributes & Library Improvements

TT
TopicTrick Team
C23 Modern C: auto, nullptr, constexpr, typeof, Attributes & Library Improvements

C23 Modern C: auto, nullptr, constexpr, typeof, Attributes & Library Improvements


Table of Contents


How to Enable C23

bash
# GCC 13+ and Clang 17+ support C23
gcc  -std=c23 -Wall -Wextra main.c -o main
clang -std=c23 -Wall -Wextra main.c -o main

# During transition (pre-standardization flag)
gcc  -std=c2x main.c  # Older GCC versions
clang -std=c2x main.c

# Check GCC version
gcc --version  # Need 13+ for good C23 support

# Check which features are available
echo '#include <limits.h>' | gcc -std=c23 -E - | grep -i stdc_version

auto: Type Inference Without the Verbosity

C23 repurposes the old, unused auto storage class specifier as a type inference keyword. The compiler deduces the type from the initializer, eliminating verbose type repetition:

c
#include <stdio.h>

// Complex iterator type — C11 style (verbose)
struct Config config = {...};
struct Config * const *iter;
for (iter = configs; iter != configs_end; iter++) { ... }

// C23 style (clean)
for (auto iter = configs; iter != configs_end; iter++) {
    printf("Config: %s\n", (*iter)->name);
}

// More examples
int main(void) {
    // Simple inference
    auto x = 42;          // int
    auto y = 3.14;        // double
    auto z = 42U;         // unsigned int
    auto w = 42LL;        // long long
    
    // With function return types
    auto config = load_config("server.conf"); // type of load_config()'s return
    
    // With complex types
    auto hash_map = hm_create(); // HashMap* — no need to write the full type
    
    printf("x=%d, y=%f\n", x, y);
    
    // auto CANNOT be used without an initializer (unlike C++ auto)
    // auto uninitialized; // ERROR
    
    return 0;
}

[!NOTE] Unlike C++, C23 auto cannot be used for function parameters or return types. It applies only to local variable declarations with initializers.


nullptr: The Type-Safe Null Pointer Constant

The old NULL macro is defined as 0 or (void*)0 — both are integers when used in non-pointer contexts, causing potential ambiguity. C23 introduces nullptr, a keyword of type nullptr_t that can only be used as a null pointer:

c
#include <stddef.h>

void old_way(void) {
    int *p = NULL;           // NULL might be 0 (integer)
    void (*fp)(void) = NULL; // NULL for function pointer
    
    // Ambiguous: is this a null pointer or the integer 0?
    // In C++, this could cause overload resolution bugs
}

void new_way(void) {
    int *p    = nullptr;         // Clearly a null pointer, not integer 0
    char *str = nullptr;         // Type: nullptr_t → implicit conversion to any pointer type
    void (*fp)(void) = nullptr;  // Works for function pointers too
    
    // Type safety checks
    // int n = nullptr;  // ERROR: nullptr cannot be assigned to non-pointer integer
    
    if (p == nullptr) {          // Comparing to nullptr — clearly null check
        printf("p is null\n");
    }
}

nullptr of type nullptr_t is implicitly convertible to any pointer type (including function pointers), but is NOT an integer — fixing the historical ambiguity.


constexpr: Compile-Time Constants

C23's constexpr is more limited than C++'s version but still extremely valuable. It declares compile-time constant variables (unlike #define, which is a preprocessor text substitution):

c
#include <stdint.h>
#include <stdbool.h>

// C23 constexpr: type-checked, scoped, debuggable constants
constexpr int    MAX_CONNECTIONS  = 1024;
constexpr double PI               = 3.14159265358979323846;
constexpr size_t CACHE_LINE_SIZE  = 64;

// Can be used in static_assert
static_assert(MAX_CONNECTIONS > 0,     "Connection limit must be positive");
static_assert(CACHE_LINE_SIZE == 64,   "Unexpected cache line size");

// Can be used in array dimensions
int connection_pool[MAX_CONNECTIONS]; // Legal: constexpr is a compile-time value

// Compared to #define:
// #define has no type → no type checking
// constexpr has full type → compiler will warn on type mismatches
// #define has no scope → leaks into all code after definition
// constexpr is block-scoped → respects C scoping rules
// #define has no debug symbol → invisible to debuggers
// constexpr has a debug symbol → visible in GDB, LLDB

int main(void) {
    constexpr int BUFFER_SIZE = 4096; // Function-scoped constexpr
    char local_buf[BUFFER_SIZE];
    
    printf("Max connections: %d\n", MAX_CONNECTIONS);
    printf("Buffer: %d bytes\n", BUFFER_SIZE);
    return 0;
}

typeof and typeof_unqual

typeof(expression) yields the type of its argument at compile time — invaluable for writing truly generic macros without requiring _Generic:

c
#include <stdio.h>

// Type-safe SWAP macro using typeof
#define SWAP(a, b) do {              \
    typeof(a) _tmp = (a);            \
    (a) = (b);                       \
    (b) = _tmp;                      \
} while (0)

// Type-safe MIN/MAX that evaluates arguments exactly once
#define MIN(a, b) ({                 \
    typeof(a) _a = (a);              \
    typeof(b) _b = (b);              \
    _a < _b ? _a : _b;               \
})

// typeof_unqual strips const/volatile qualifiers
void example(const int *const ptr) {
    typeof(*ptr) local_val = 42;      // type: const int
    typeof_unqual(*ptr) mutable = 42; // type: int (without const)
}

int main(void) {
    int  x = 10, y = 20;
    SWAP(x, y);
    printf("x=%d, y=%d\n", x, y);  // x=20, y=10
    
    double a = 3.14, b = 2.71;
    SWAP(a, b); // Works with doubles too — typeof infers correct type
    printf("a=%f, b=%f\n", a, b);
    
    printf("min(5, 3) = %d\n", MIN(5, 3));   // 3
    return 0;
}

Standardized Attributes

C23 standardizes attributes using [[attribute]] syntax (borrowed from C++). Previously, compilers had vendor-specific syntax (__attribute__((...))):

c
#include <stdbool.h>

// [[nodiscard]]: compiler warning if return value is discarded
[[nodiscard]] int connect_to_server(const char *host, int port) {
    // ...
    return 0;
}

// [[nodiscard("reason")]]: with explanation
[[nodiscard("Returns error code — check before proceeding")]]
int write_transaction_log(const char *data);

// [[maybe_unused]]: suppress "unused variable" warnings
void process([[maybe_unused]] int debug_mode, const char *data) {
    // debug_mode only used in debug builds
}

// [[deprecated]]: mark as deprecated with message
[[deprecated("Use new_api() instead")]]
void old_api(void);

// [[fallthrough]]: intentional switch fallthrough (already in C17 but formalized)
void state_machine(int state) {
    switch (state) {
        case 0:
            setup();
            [[fallthrough]]; // Intentional — drop to case 1
        case 1:
            run();
            break;
        case 2:
            cleanup();
            break;
    }
}

// [[likely]] / [[unlikely]]: branch prediction hints
void handle_request(int type) {
    if (type == REQUEST_NORMAL) [[likely]] {
        handle_normal(type);
    } else [[unlikely]] {
        handle_error(type);
    }
}

// [[noreturn]]: function never returns
[[noreturn]] void fatal_error(const char *msg) {
    fprintf(stderr, "FATAL: %s\n", msg);
    abort();
}

Enhanced Numerics: Binary Literals and Digit Separators

c
// Binary literals (0b prefix) — now standard in C23
uint8_t  status_reg = 0b10110100;
uint32_t bitmask    = 0b11111111000011110000111100001111;

// Digit separators with ' (apostrophe) — improves readability
uint64_t max_memory  = 1'000'000'000;    // 1 billion
double   avogadro    = 6.022'141'99e23;
int      hex_value   = 0xFF'FF'FF'FF;
int      bin_value   = 0b1010'1010'1010; // Group by nibble

// Before C23, you'd write: 1000000000 (easy to miscnt zeros)
// With separators: 1'000'000'000 (instantly readable)

Improved Standard Library

memset_explicit: Secure Zeroing

Regular memset for zeroing sensitive data (passwords, encryption keys) can be optimized away by the compiler since the memory is "dead" after zeroing. memset_explicit is guaranteed never to be optimized away:

c
#include <string.h>

void zero_sensitive_data(void *buf, size_t len) {
    // OLD (may be optimized away in release builds):
    memset(buf, 0, len);
    
    // C23 (guaranteed to execute):
    memset_explicit(buf, 0, len);
}

void clear_password(char *password, size_t len) {
    memset_explicit(password, 0, len); // Always zeroed — compiler cannot remove
}

strdup and strndup (Standardized)

Previously POSIX extensions, now part of C23:

c
#include <string.h>
#include <stdlib.h>

char *copy = strdup("Hello, C23!");  // malloc + strcpy in one
if (!copy) { /* handle OOM */ }
printf("%s\n", copy);
free(copy);

char *partial = strndup("Hello, World!", 5); // "Hello" — maximum 5 chars
free(partial);

stdbit.h: Portable Bit Operations

C23 adds <stdbit.h> with standardized, efficient bit counting and manipulation functions — replacing the need for vendor-specific __builtin_popcount etc.:

c
#include <stdbit.h>
#include <stdint.h>
#include <stdio.h>

int main(void) {
    uint32_t value = 0b10110100;
    
    // Count leading zeros
    printf("Leading zeros: %u\n",  stdc_leading_zeros_ui(value));  // 24
    
    // Count trailing zeros
    printf("Trailing zeros: %u\n", stdc_trailing_zeros_ui(value)); // 2
    
    // Count set bits (popcount)  
    printf("Set bits: %u\n",       stdc_count_ones_ui(value));     // 4
    
    // Count zero bits
    printf("Zero bits: %u\n",      stdc_count_zeros_ui(value));    // 28
    
    // Test bit
    printf("Bit 4 set: %d\n",      stdc_has_single_bit_ui(16));    // 1 (power of 2)
    
    // Next power of 2
    printf("Next pow2(100): %u\n", stdc_bit_ceil_ui(100));         // 128
    printf("Prev pow2(100): %u\n", stdc_bit_floor_ui(100));        // 64
    
    // Width (position of highest set bit + 1)
    printf("Bit width: %u\n",      stdc_bit_width_ui(value));      // 8
    
    return 0;
}

These functions compile to single CPU instructions on x86-64 (POPCNT, BSR, LZCNT, TZCNT).


Removed and Deprecated Features

C23 removes features that were already deprecated or "optional" in C11:

RemovedReplacement
K&R style function definitions (int f(a,b) int a, b; {...})Modern: int f(int a, int b) {...}
Implicit int return typeAlways specify return type explicitly
gets() (already removed in C11)Use fgets()
Trigraph sequences (??= → #)Use actual characters
Unprototyped function declarationsAlways prototype functions

C23 vs C11 vs C99: Migration Guide

FeatureC99C11C23
bool, true, falseMacro via stdbool.hKeywordBuilt-in keyword
_Generic❌✅✅ Improved
nullptr❌❌✅
constexpr❌❌✅
auto (type inference)❌❌✅
typeofGNU extensionGNU extension✅ Standardized
Binary literals 0b...GNU extensionGNU extension✅ Standardized
Digit separators 1'000❌❌✅
[[attributes]]❌Mixed✅ Full set
memset_explicit❌❌✅
strdupPOSIXPOSIX✅ Standardized
<stdbit.h>❌❌✅

Frequently Asked Questions

Is C23 backwards compatible with C11/C99 code? Almost entirely. The only breaking changes are removal of K&R style function definitions and trigraphs — both of which are extremely rare in modern code. Virtually all C11 and C99 code compiles cleanly under C23.

When will compilers fully support C23? GCC 14+ and Clang 18+ have comprehensive C23 support. MSVC (Microsoft Visual C++) has partial support. Check cppreference.com's C23 compiler support table for the current status per feature.

Can I use C23's auto in the same file as C++ code? No — C and C++ are compiled separately. auto in C23 means type inference (same as in C++11+), but the rules differ. In C++, auto can be used for function return types and template parameters; in C23, it cannot.

Is constexpr in C23 the same as in C++? No. C23's constexpr is more limited — it can only be applied to variables and only for values that can be computed at compile time from constant expressions. C++'s constexpr can additionally be applied to functions and enables compile-time function evaluation.


Key Takeaway

C23 demonstrates that C is still actively evolving. By adopting auto for type inference, nullptr for null safety, constexpr for zero-cost compile-time constants, typeof for powerful generic macros, and standardized [[attributes]] for optimization hints, you get a modern, expressive language that retains every bit of C's legendary performance.

These are not cosmetic changes — they directly reduce bugs (nullptr prevents null-as-integer confusion), improve tooling (constexpr variables are visible in debuggers), and enable better compiler optimization (attributes help the compiler understand your code's intent).

Read next: C Security & Hardening: Defeating Buffer Overflows →


Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.