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
- auto: Type Inference Without the Verbosity
- nullptr: The Type-Safe Null Pointer Constant
- constexpr: Compile-Time Constants
- typeof and typeof_unqual
- Standardized Attributes
- Enhanced Numerics: Binary Literals and Digit Separators
- Improved Standard Library
- stdbit.h: Portable Bit Operations
- Removed and Deprecated Features
- C23 vs C11 vs C99: Migration Guide
- Frequently Asked Questions
- Key Takeaway
How to Enable C23
# 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_versionauto: 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:
#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
autocannot 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:
#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):
#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:
#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__((...))):
#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
// 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:
#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:
#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.:
#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:
| Removed | Replacement |
|---|---|
K&R style function definitions (int f(a,b) int a, b; {...}) | Modern: int f(int a, int b) {...} |
| Implicit int return type | Always specify return type explicitly |
gets() (already removed in C11) | Use fgets() |
Trigraph sequences (??= → #) | Use actual characters |
| Unprototyped function declarations | Always prototype functions |
C23 vs C11 vs C99: Migration Guide
| Feature | C99 | C11 | C23 |
|---|---|---|---|
bool, true, false | Macro via stdbool.h | Keyword | Built-in keyword |
_Generic | ⌠| ✅ | ✅ Improved |
nullptr | ⌠| ⌠| ✅ |
constexpr | ⌠| ⌠| ✅ |
auto (type inference) | ⌠| ⌠| ✅ |
typeof | GNU extension | GNU extension | ✅ Standardized |
Binary literals 0b... | GNU extension | GNU extension | ✅ Standardized |
Digit separators 1'000 | ⌠| ⌠| ✅ |
[[attributes]] | ⌠| Mixed | ✅ Full set |
memset_explicit | ⌠| ⌠| ✅ |
strdup | POSIX | POSIX | ✅ 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.
