C23 Standard: Modern Features Complete Guide for Developers
Discover the C23 standard's modern features: nullptr, bool, attributes, digit separators, and more.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of C programming (variables, functions, pointers).
- ✓Familiarity with C11 or earlier standards is helpful but not required.
- ✓A compiler that supports C23 (GCC 14+, Clang 17+).
- C23 introduces
nullptras a keyword for null pointers, replacingNULLmacro. bool,true, andfalsebecome keywords, withboolreplacing_Bool.- Attributes like
[[deprecated]]and[[nodiscard]]are standardized. - Digit separators (
') improve readability of large numbers. #elifdefand#elifndefpreprocessor directives simplify conditional compilation.
Think of C23 as a renovation of a classic house. The old house had workarounds like using a macro for 'nothing' (NULL) and a special type for true/false (_Bool). C23 adds proper doors and windows: nullptr is a dedicated 'nothing' key, bool is a real room name, and [[attributes]] are like labels that tell you which parts are fragile or should not be ignored. Digit separators are like commas in large numbers (e.g., 1'000'000) making them easier to read at a glance.
The C programming language has been the backbone of systems programming for decades. With the release of the C23 standard (ISO/IEC 9899:2023), C receives its most significant update since C11, bringing modern features that improve safety, readability, and developer productivity. If you've been writing C code with the C11 or earlier standards, you'll find that C23 addresses long-standing pain points: the ambiguity of NULL, the awkwardness of _Bool, and the lack of standardized annotations for compilers. This guide explores the key features of C23 with practical, production-ready examples. You'll learn how to use nullptr to avoid pointer-related bugs, how the new bool type simplifies Boolean logic, how attributes like [[nodiscard]] enforce correct API usage, and how digit separators make numeric literals more readable. We also cover the new preprocessor directives #elifdef and #elifndef, and the unreachable() macro for optimization. By the end, you'll be equipped to write safer, cleaner C code that leverages the best of modern C. Whether you're maintaining legacy systems or starting a new project, understanding C23 is essential for staying current in the C ecosystem.
1. nullptr: The Type-Safe Null Pointer
One of the most anticipated features in C23 is the introduction of nullptr as a keyword. In previous standards, null pointers were represented by the macro NULL, which is typically defined as ((void*)0) or 0. This caused ambiguity in function overloading (in C++) and could lead to subtle bugs when passing NULL to variadic functions or in contexts where an integer zero is expected. C23's nullptr is a keyword of type nullptr_t, which implicitly converts to any pointer type but not to integer types. This eliminates the ambiguity and improves type safety. For example:
``c int *ptr = nullptr; // OK, implicit conversion int x = nullptr; // Compiler error: cannot convert nullptr_t to int ``
To use nullptr, ensure your compiler supports C23 (GCC 14+, Clang 17+). The following example demonstrates its usage in a function that returns a pointer:
```c #include <stdio.h>
int find_value(int arr, int size, int target) { for (int i = 0; i < size; i++) { if (arr[i] == target) { return &arr[i]; } } return nullptr; // Use nullptr instead of NULL }
int main() { int data[] = {1, 2, 3, 4, 5}; int result = find_value(data, 5, 3); if (result != nullptr) { printf("Found: %d ", result); } else { printf("Not found "); } return 0; } ```
Output: `` Found: 3 ``
Using nullptr makes the code's intent clearer and prevents accidental misuse. It is recommended to replace all uses of NULL with nullptr in new C23 code.
NULL with nullptr can catch bugs where NULL was passed to a function expecting an integer (e.g., printf("%d", NULL)). With nullptr, such code will not compile.nullptr instead of NULL for type-safe null pointers. It prevents accidental conversion to integer types.2. bool: A Proper Boolean Type
C23 promotes bool, true, and false to keywords, making them first-class citizens of the language. Previously, C used _Bool as the boolean type, with bool, true, and false defined as macros in <stdbool.h>. Now, you can use bool directly without including any header, and true and false are keywords as well. This simplifies code and aligns C with C++ in terms of boolean handling. The bool type is still an unsigned integer type that can hold only 0 or 1, but the keyword usage makes code cleaner. Example:
```c #include <stdio.h>
bool is_even(int n) { return n % 2 == 0; }
int main() { int num = 4; if (is_even(num)) { printf("%d is even ", num); } else { printf("%d is odd ", num); } return 0; } ```
Output: `` 4 is even ``
Note that _Bool is still available for backward compatibility, but the new keywords are preferred. The header <stdbool.h> now simply defines bool as _Bool (which is the same keyword) for compatibility with older code. In C23, you can write bool flag = true; without any includes.
_Bool with bool and remove #include <stdbool.h> if not needed. This reduces dependencies and modernizes the codebase.bool a keyword, eliminating the need for <stdbool.h>. Use bool, true, and false directly.3. Attributes: Standardized Annotations
C23 introduces a standardized attribute syntax using double square brackets [[attribute]]. This feature, borrowed from C++, allows programmers to give hints to the compiler about code behavior, such as deprecation, unused results, or fallthrough in switch statements. The most useful attributes in C23 are:
[[deprecated]]: Marks a function or variable as deprecated, causing a warning when used.[[deprecated("message")]]: Provides a custom deprecation message.[[nodiscard]]: Warns if the return value of a function is ignored.[[maybe_unused]]: Suppresses warnings about unused variables or functions.[[fallthrough]]: Indicates intentional fallthrough in a switch case.
Example:
```c #include <stdio.h>
[[nodiscard]] int compute_value() { return 42; }
[[deprecated("Use new_function instead")]] void old_function() { // ... }
int main() { // Warning: ignoring return value of 'compute_value' compute_value(); // Warning: 'old_function' is deprecated old_function(); return 0; } ```
When compiled with warnings enabled (e.g., -Wall), the compiler will emit warnings for the above code. Attributes help enforce coding standards and catch potential bugs early.
[[nodiscard]] to functions that allocate resources (e.g., malloc) can prevent memory leaks by forcing callers to check the return value.[[nodiscard]] to enforce checking return values, [[deprecated]] to mark obsolete functions, and [[maybe_unused]] to suppress warnings.4. Digit Separators: Readable Numeric Literals
C23 allows single quotes (') as digit separators in numeric literals to improve readability. This is especially useful for large numbers, bit patterns, and hexadecimal constants. The separator can be placed between digits but not at the beginning or end. Examples:
``c int million = 1'000'000; // 1,000,000 long long big = 123'456'789'012; // 123,456,789,012 unsigned int mask = 0xFF'00'FF; // 0xFF00FF float pi = 3.141'592'653; // 3.141592653 ``
Digit separators have no effect on the value; they are purely cosmetic. They help avoid off-by-one errors when counting digits. Example usage:
```c #include <stdio.h>
int main() { int population = 8'000'000'000; // 8 billion printf("World population: %d ", population); unsigned int color = 0x00'FF'00; // Green printf("Green color code: 0x%06X ", color); return 0; } ```
Output: `` World population: 8000000000 Green color code: 0x00FF00 ``
Note that the output does not include separators; they are only in the source code.
0b1000'0000) can make bitwise operations clearer and reduce errors.') make large numeric literals easier to read. They are ignored by the compiler.5. Preprocessor Enhancements: #elifdef and #elifndef
C23 adds #elifdef and #elifndef directives to the preprocessor, complementing the existing #ifdef and #ifndef. These allow for more concise conditional compilation chains. Previously, to check for multiple macros, you had to use nested #ifdef or combine with #elif defined(...). Now you can write:
``c #ifdef FEATURE_A // code for A #elifdef FEATURE_B // code for B #elifndef FEATURE_C // code if C is not defined #else // default code #endif ``
This is equivalent to:
``c #if defined(FEATURE_A) // code for A #elif defined(FEATURE_B) // code for B #elif !defined(FEATURE_C) // code if C is not defined #else // default code #endif ``
The new directives reduce boilerplate and improve readability. Example:
```c #include <stdio.h>
#define PLATFORM_LINUX
int main() { #ifdef PLATFORM_WINDOWS printf("Windows "); #elifdef PLATFORM_LINUX printf("Linux "); #elifdef PLATFORM_MAC printf("Mac "); #else printf("Unknown platform "); #endif return 0; } ```
Output: `` Linux ``
This feature simplifies platform-specific code and configuration management.
#elifdef can reduce nesting and make it easier to add new platform branches without breaking existing logic.#elifdef and #elifndef to write cleaner conditional compilation chains.6. The unreachable() Macro
C23 introduces the macro, defined in unreachable()<stddef.h>, to mark code paths that should never be executed. This is a hint to the compiler for optimization and can help suppress warnings about uninitialized variables or missing return statements. If the code is actually reached, behavior is undefined (similar to _ in GCC/Clang). Example:_builtin_unreachable()
```c #include <stdio.h> #include <stddef.h> // for unreachable()
enum Color { RED, GREEN, BLUE };
const char* color_name(enum Color c) { switch (c) { case RED: return "Red"; case GREEN: return "Green"; case BLUE: return "Blue"; default: unreachable(); // All cases covered } }
int main() { printf("%s ", color_name(RED)); return 0; } ```
Output: `` Red ``
Without , the compiler might warn about control reaching the end of a non-void function. Using it tells the compiler that the default case is impossible, allowing better optimization and cleaner code.unreachable()
unreachable() unless you have formal proof that the path is unreachable. Otherwise, use an assertion or error handling instead.unreachable() to mark impossible code paths, enabling better compiler optimizations and cleaner code.7. Other Notable Features
C23 includes several other improvements that enhance the language's expressiveness and safety:
#warningdirective: Emits a warning during preprocessing, useful for deprecation notices.#embeddirective: (Optional) Embeds binary files as byte arrays at compile time.typeofoperator: Returns the type of an expression, similar todecltypein C++.constexpr: (Not yet in C23, but planned for future) – C23 does not includeconstexpr; that is expected in C2Y.static_assertwith no message:static_assert(condition)is now allowed without a message string.alignofandalignas: Standardized alignment control (already in C11, but refined).
Example of #warning:
``c #warning "This code is deprecated, use new version" ``
Example of typeof:
```c #include <stdio.h>
int main() { int x = 10; typeof(x) y = 20; // y is int printf("%d ", y); return 0; } ```
Output: `` 20 ``
These features, while not as groundbreaking as nullptr or bool, contribute to a more modern and robust C programming experience.
#warning to mark deprecated code paths during migration, making it easier to track technical debt.#warning, typeof, and static_assert improvements. Explore them to write better code.The NULL Dereference That Crashed a Satellite
NULL macro for null pointer comparison, but a function returned a non-zero error code disguised as a valid pointer. The NULL check passed because the pointer was not zero, but the memory was invalid.NULL with nullptr and added explicit checks using if (ptr == nullptr) after every allocation. Also added [[nodiscard]] to the allocation function to force callers to check the return value.- Always check pointer return values from allocation functions.
- Use
nullptrinstead ofNULLfor clarity and type safety. - Use
[[nodiscard]]attribute to enforce checking return values. - Never assume memory allocation succeeds.
- In embedded systems, consider using static analysis tools to catch null pointer issues.
NULL with nullptr to ensure type safety.[[nodiscard]] to the function declaration and check return value._Bool vs int confusionbool keyword and include <stdbool.h> for compatibility.1'000'000 for clarity.#ifdef chains becomes messy#elifdef and #elifndef to simplify preprocessor logic.gcc -std=c23 -o test test.cclang -std=c23 -o test test.c#define nullptr ((void*)0) as a temporary workaround.| File | Command / Code | Purpose |
|---|---|---|
| nullptr_example.c | int* find_value(int *arr, int size, int target) { | 1. nullptr |
| bool_example.c | bool is_even(int n) { | 2. bool |
| attributes_example.c | [[nodiscard]] int compute_value() { | 3. Attributes |
| digit_separators.c | int main() { | 4. Digit Separators |
| elifdef_example.c | int main() { | 5. Preprocessor Enhancements |
| unreachable_example.c | enum Color { RED, GREEN, BLUE }; | 6. The unreachable() Macro |
| typeof_example.c | int main() { | 7. Other Notable Features |
Key takeaways
nullptr for type-safe null pointers, bool as a keyword, and standardized attributes like [[nodiscard]].') improve readability of large numeric literals.#elifdef and #elifndef simplify conditional compilation.unreachable() macro marks impossible code paths for optimization.Common mistakes to avoid
5 patternsUsing `NULL` in C23 code instead of `nullptr`
Forgetting to include `<stddef.h>` for `unreachable()`
Using `bool` as a variable name in C23
Placing digit separators at the beginning or end of a number
Assuming `#elifdef` works in older compilers
Interview Questions on This Topic
What is the purpose of `nullptr` in C23 and how does it improve type safety?
nullptr is a keyword representing a null pointer constant. Unlike NULL, which can be implicitly converted to integer types, nullptr is of type nullptr_t and only converts to pointer types. This prevents accidental misuse in contexts like variadic functions or integer arithmetic.Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C Basics. Mark it forged?
5 min read · try the examples if you haven't