Home C / C++ C23 Standard: Modern Features Complete Guide for Developers
Intermediate 5 min · July 13, 2026

C23 Standard: Modern Features Complete Guide for Developers

Discover the C23 standard's modern features: nullptr, bool, attributes, digit separators, and more.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • 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+).
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • C23 introduces nullptr as a keyword for null pointers, replacing NULL macro.
  • bool, true, and false become keywords, with bool replacing _Bool.
  • Attributes like [[deprecated]] and [[nodiscard]] are standardized.
  • Digit separators (') improve readability of large numbers.
  • #elifdef and #elifndef preprocessor directives simplify conditional compilation.
✦ Definition~90s read
What is C23 Standard?

C23 is the latest ISO standard for the C programming language, introducing modern features like nullptr, bool keywords, attributes, digit separators, and enhanced preprocessor directives to improve safety and readability.

Think of C23 as a renovation of a classic house.
Plain-English First

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.

nullptr_example.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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;
}

int main() {
    int data[] = {1, 2, 3, 4, 5};
    int *result = find_value(data, 5, 3);
    if (result != nullptr) {
        printf("Found: %d\n", *result);
    } else {
        printf("Not found\n");
    }
    return 0;
}
Output
Found: 3
🔥Compatibility Note
📊 Production Insight
In production, replacing 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.
🎯 Key Takeaway
Use 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_example.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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\n", num);
    } else {
        printf("%d is odd\n", num);
    }
    return 0;
}
Output
4 is even
💡Best Practice
📊 Production Insight
When migrating legacy code, replace _Bool with bool and remove #include <stdbool.h> if not needed. This reduces dependencies and modernizes the codebase.
🎯 Key Takeaway
C23 makes 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.

```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.

attributes_example.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

[[nodiscard]] int compute_value() {
    return 42;
}

[[deprecated("Use new_function instead")]]
void old_function() {
    // ...
}

int main() {
    compute_value();  // Warning: ignoring return value
    old_function();   // Warning: deprecated
    return 0;
}
Output
Compiler warnings (not runtime output)
⚠ Compiler Support
📊 Production Insight
In large codebases, adding [[nodiscard]] to functions that allocate resources (e.g., malloc) can prevent memory leaks by forcing callers to check the return value.
🎯 Key Takeaway
Use [[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.

digit_separators.cC
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
    int population = 8'000'000'000;
    printf("World population: %d\n", population);
    
    unsigned int color = 0x00'FF'00;
    printf("Green color code: 0x%06X\n", color);
    
    return 0;
}
Output
World population: 8000000000
Green color code: 0x00FF00
💡Readability Tip
📊 Production Insight
In embedded systems, using digit separators for bit masks (e.g., 0b1000'0000) can make bitwise operations clearer and reduce errors.
🎯 Key Takeaway
Digit separators (') 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 ``

``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_example.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

#define PLATFORM_LINUX

int main() {
#ifdef PLATFORM_WINDOWS
    printf("Windows\n");
#elifdef PLATFORM_LINUX
    printf("Linux\n");
#elifdef PLATFORM_MAC
    printf("Mac\n");
#else
    printf("Unknown platform\n");
#endif
    return 0;
}
Output
Linux
🔥Backward Compatibility
📊 Production Insight
In cross-platform codebases, #elifdef can reduce nesting and make it easier to add new platform branches without breaking existing logic.
🎯 Key Takeaway
Use #elifdef and #elifndef to write cleaner conditional compilation chains.

6. The unreachable() Macro

C23 introduces the unreachable() macro, defined in <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 __builtin_unreachable() in GCC/Clang). Example:

```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 unreachable(), 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_example.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stddef.h>

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();
    }
}

int main() {
    printf("%s\n", color_name(RED));
    return 0;
}
Output
Red
⚠ Undefined Behavior
📊 Production Insight
In safety-critical systems, avoid unreachable() unless you have formal proof that the path is unreachable. Otherwise, use an assertion or error handling instead.
🎯 Key Takeaway
Use 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:

  • #warning directive: Emits a warning during preprocessing, useful for deprecation notices.
  • #embed directive: (Optional) Embeds binary files as byte arrays at compile time.
  • typeof operator: Returns the type of an expression, similar to decltype in C++.
  • constexpr: (Not yet in C23, but planned for future) – C23 does not include constexpr; that is expected in C2Y.
  • static_assert with no message: static_assert(condition) is now allowed without a message string.
  • alignof and alignas: Standardized alignment control (already in C11, but refined).

``c #warning "This code is deprecated, use new version" ``

```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.

typeof_example.cC
1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
    int x = 10;
    typeof(x) y = 20;
    printf("%d\n", y);
    return 0;
}
Output
20
🔥Future Directions
📊 Production Insight
Use #warning to mark deprecated code paths during migration, making it easier to track technical debt.
🎯 Key Takeaway
C23 brings many small but useful features like #warning, typeof, and static_assert improvements. Explore them to write better code.
● Production incidentPOST-MORTEMseverity: high

The NULL Dereference That Crashed a Satellite

Symptom
Satellite telemetry showed intermittent system resets and data corruption.
Assumption
The developer assumed that a pointer returned from a memory allocation function was always valid.
Root cause
The code used 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.
Fix
Replaced 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.
Key lesson
  • Always check pointer return values from allocation functions.
  • Use nullptr instead of NULL for 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.
Production debug guideSymptom to Action5 entries
Symptom · 01
Compiler warning about implicit conversion from integer to pointer
Fix
Replace NULL with nullptr to ensure type safety.
Symptom · 02
Function return value ignored leading to resource leak
Fix
Add [[nodiscard]] to the function declaration and check return value.
Symptom · 03
Boolean logic errors due to _Bool vs int confusion
Fix
Use bool keyword and include <stdbool.h> for compatibility.
Symptom · 04
Large numeric literals hard to read and prone to off-by-one errors
Fix
Use digit separators like 1'000'000 for clarity.
Symptom · 05
Conditional compilation with many #ifdef chains becomes messy
Fix
Use #elifdef and #elifndef to simplify preprocessor logic.
★ Quick Debug Cheat SheetCommon issues and immediate fixes for C23 features.
Compiler error: 'nullptr' undeclared
Immediate action
Ensure compiler supports C23 (e.g., GCC 14+, Clang 17+). Add `-std=c23` flag.
Commands
gcc -std=c23 -o test test.c
clang -std=c23 -o test test.c
Fix now
Use #define nullptr ((void*)0) as a temporary workaround.
Warning: 'bool' is a C23 keyword+
Immediate action
Include `<stdbool.h>` for backward compatibility, or rename variables.
Commands
#include <stdbool.h>
gcc -std=c23 -Wall -o test test.c
Fix now
Use _Bool if compiler doesn't support C23 fully.
Attribute 'nodiscard' ignored+
Immediate action
Check compiler version and add `-Wno-ignored-attributes` if needed.
Commands
gcc -std=c23 -Wall -Werror -o test test.c
clang -std=c23 -Weverything -o test test.c
Fix now
Use __attribute__((warn_unused_result)) for GCC/Clang.
Digit separator ' causing syntax error+
Immediate action
Ensure compiler supports C23 digit separators.
Commands
gcc -std=c23 -o test test.c
clang -std=c23 -o test test.c
Fix now
Remove separators or use 1e6 for floating point.
unreachable() macro not found+
Immediate action
Include `<stddef.h>` or define your own: `#define unreachable() __builtin_unreachable()`
Commands
#include <stddef.h>
gcc -std=c23 -O2 -o test test.c
Fix now
Use __builtin_unreachable() directly.
FeatureC11/C17C23
Null pointer constantNULL macro (integer or void*)nullptr keyword (type-safe)
Boolean type_Bool with <stdbool.h> macrosbool keyword (no header needed)
AttributesNon-standard (GCC __attribute__)Standard [[...]] syntax
Digit separatorsNot supportedSingle quote ' separator
Preprocessor elifdefNot supported#elifdef and #elifndef
unreachable()Not standard (compiler builtins)Standard macro in <stddef.h>
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
nullptr_example.cint* find_value(int *arr, int size, int target) {1. nullptr
bool_example.cbool is_even(int n) {2. bool
attributes_example.c[[nodiscard]] int compute_value() {3. Attributes
digit_separators.cint main() {4. Digit Separators
elifdef_example.cint main() {5. Preprocessor Enhancements
unreachable_example.cenum Color { RED, GREEN, BLUE };6. The unreachable() Macro
typeof_example.cint main() {7. Other Notable Features

Key takeaways

1
C23 introduces nullptr for type-safe null pointers, bool as a keyword, and standardized attributes like [[nodiscard]].
2
Digit separators (') improve readability of large numeric literals.
3
New preprocessor directives #elifdef and #elifndef simplify conditional compilation.
4
The unreachable() macro marks impossible code paths for optimization.
5
C23 is largely backward compatible; migrate gradually to leverage modern features.

Common mistakes to avoid

5 patterns
×

Using `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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the purpose of `nullptr` in C23 and how does it improve type saf...
Q02SENIOR
Explain how attributes like `[[nodiscard]]` can prevent bugs in producti...
Q03JUNIOR
How do digit separators work in C23? Provide an example where they impro...
Q04SENIOR
What are the new preprocessor directives in C23 and how do they simplify...
Q05SENIOR
Describe a scenario where using `unreachable()` can improve performance.
Q01 of 05JUNIOR

What is the purpose of `nullptr` in C23 and how does it improve type safety?

ANSWER
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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is C23 backward compatible with C11?
02
Do I need to include any headers to use `bool` in C23?
03
How do I enable C23 support in GCC or Clang?
04
Can I use digit separators in floating-point literals?
05
What is the difference between `nullptr` and `NULL`?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C Basics. Mark it forged?

5 min read · try the examples if you haven't

Previous
Function Pointers in C
18 / 24 · C Basics
Next
Memory Safety in C: Secure Coding Practices