Skip to main content

Modern C Standards

C continues to evolve. Modern C (C11, C17, C23) adds powerful features for type safety, concurrency, and expressiveness while maintaining backward compatibility.

C Standards Timeline

C89/C90 ──► C99 ──► C11 ──► C17 ──► C23
│          │       │       │       │
ANSI C     VLA     Atomics Bug     Attributes
           inline  Threads fixes   typeof
           <stdbool.h>     Type-   nullptr
           // comments    generic constexpr
Compiler Support: GCC 13+ and Clang 16+ support most C23 features. Use -std=c23 to enable. For C11/C17, use -std=c11 or -std=c17.

C11 Features

_Static_assert - Compile-Time Assertions

#include <limits.h>

// Verify assumptions at compile time
_Static_assert(sizeof(int) >= 4, "int must be at least 32 bits");
_Static_assert(CHAR_BIT == 8, "byte must be 8 bits");

// Check struct size/alignment for binary compatibility
struct NetworkPacket {
    uint32_t id;
    uint16_t flags;
    uint16_t length;
};
_Static_assert(sizeof(struct NetworkPacket) == 8, 
               "NetworkPacket must be exactly 8 bytes");

// Ensure enum fits in expected storage
typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_STOPPED,
    STATE_COUNT
} State;
_Static_assert(STATE_COUNT <= 256, "State must fit in uint8_t");

// Check array size
#define ARRAY_SIZE 100
int array[ARRAY_SIZE];
_Static_assert(ARRAY_SIZE <= 1000, "Array too large for stack");

// C23 simplifies to just 'static_assert' without message
// static_assert(sizeof(int) >= 4);  // C23

_Generic - Type-Generic Macros

#include <stdio.h>
#include <math.h>

// Type-safe generic macro - picks function based on argument type
#define abs_value(x) _Generic((x), \
    int:         abs,               \
    long:        labs,              \
    long long:   llabs,             \
    float:       fabsf,             \
    double:      fabs,              \
    long double: fabsl              \
)(x)

// Type-safe print macro
#define print_value(x) _Generic((x), \
    int:         printf("%d\n", x),                    \
    unsigned:    printf("%u\n", x),                    \
    long:        printf("%ld\n", x),                   \
    double:      printf("%f\n", x),                    \
    char*:       printf("%s\n", x),                    \
    const char*: printf("%s\n", x),                    \
    default:     printf("Unknown type\n")              \
)

// Type name as string (debugging)
#define typename(x) _Generic((x), \
    _Bool:       "bool",           \
    char:        "char",           \
    int:         "int",            \
    long:        "long",           \
    float:       "float",          \
    double:      "double",         \
    char*:       "char*",          \
    void*:       "void*",          \
    default:     "unknown"         \
)

// Math functions that work on any numeric type
#define square(x) _Generic((x), \
    int:    (x) * (x),          \
    float:  (x) * (x),          \
    double: (x) * (x)           \
)

int main(void) {
    int i = -42;
    double d = -3.14;
    
    printf("abs(%d) = %d\n", i, abs_value(i));    // Uses abs()
    printf("abs(%f) = %f\n", d, abs_value(d));    // Uses fabs()
    
    print_value(42);        // %d
    print_value(3.14);      // %f
    print_value("hello");   // %s
    
    printf("Type of 42: %s\n", typename(42));     // "int"
    printf("Type of 3.14: %s\n", typename(3.14)); // "double"
    
    return 0;
}

_Alignas and _Alignof - Alignment Control

#include <stdio.h>
#include <stdalign.h>  // Provides alignas and alignof macros

// Force alignment for SIMD, cache lines, or hardware requirements
struct alignas(16) SIMDVector {
    float x, y, z, w;
};

// Cache-line aligned to prevent false sharing
struct alignas(64) ThreadData {
    int counter;
    // Padding happens automatically
};

// Check alignment requirements
int main(void) {
    printf("int alignment: %zu\n", alignof(int));         // Usually 4
    printf("double alignment: %zu\n", alignof(double));   // Usually 8
    printf("SIMDVector alignment: %zu\n", alignof(struct SIMDVector)); // 16
    
    // Stack variables with alignment
    alignas(32) char buffer[256];  // 32-byte aligned buffer
    
    // Verify alignment at runtime
    if ((uintptr_t)buffer % 32 == 0) {
        printf("Buffer is properly aligned\n");
    }
    
    return 0;
}

// Aligned allocation (C11)
#include <stdlib.h>

void *aligned_alloc(size_t alignment, size_t size);

void example(void) {
    // Allocate 1024 bytes, 64-byte aligned
    void *ptr = aligned_alloc(64, 1024);
    // size must be multiple of alignment
    free(ptr);
}

_Atomic Types and <stdatomic.h>

#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>  // C11 threads

// Atomic counter - no locks needed for simple operations
atomic_int counter = 0;

int thread_func(void *arg) {
    for (int i = 0; i < 1000000; i++) {
        atomic_fetch_add(&counter, 1);  // Atomic increment
    }
    return 0;
}

// Atomic flag for spinlocks
atomic_flag lock = ATOMIC_FLAG_INIT;

void spinlock_acquire(atomic_flag *lock) {
    while (atomic_flag_test_and_set_explicit(lock, memory_order_acquire)) {
        // Spin
    }
}

void spinlock_release(atomic_flag *lock) {
    atomic_flag_clear_explicit(lock, memory_order_release);
}

// Compare-and-swap for lock-free data structures
atomic_int head = 0;

void push(int value) {
    int expected = atomic_load(&head);
    int desired = value;
    
    while (!atomic_compare_exchange_weak(&head, &expected, desired)) {
        // CAS failed, retry with new expected value
    }
}

// Memory ordering options
// memory_order_relaxed  - No ordering guarantees
// memory_order_acquire  - No reads/writes move before this load
// memory_order_release  - No reads/writes move after this store
// memory_order_acq_rel  - Both acquire and release
// memory_order_seq_cst  - Full sequential consistency (default)

int main(void) {
    thrd_t threads[4];
    
    for (int i = 0; i < 4; i++) {
        thrd_create(&threads[i], thread_func, NULL);
    }
    
    for (int i = 0; i < 4; i++) {
        thrd_join(threads[i], NULL);
    }
    
    printf("Counter: %d (expected: 4000000)\n", atomic_load(&counter));
    
    return 0;
}

C11 Threads (<threads.h>)

#include <stdio.h>
#include <threads.h>

// Thread-local storage
thread_local int tls_value = 0;

// Mutex
mtx_t mutex;

// Condition variable
cnd_t condition;
int ready = 0;

int producer(void *arg) {
    mtx_lock(&mutex);
    ready = 1;
    cnd_signal(&condition);  // Wake one waiter
    mtx_unlock(&mutex);
    return 0;
}

int consumer(void *arg) {
    mtx_lock(&mutex);
    while (!ready) {
        cnd_wait(&condition, &mutex);  // Wait and release mutex
    }
    printf("Data ready!\n");
    mtx_unlock(&mutex);
    return 0;
}

int main(void) {
    mtx_init(&mutex, mtx_plain);
    cnd_init(&condition);
    
    thrd_t prod, cons;
    thrd_create(&cons, consumer, NULL);
    thrd_create(&prod, producer, NULL);
    
    thrd_join(prod, NULL);
    thrd_join(cons, NULL);
    
    mtx_destroy(&mutex);
    cnd_destroy(&condition);
    
    return 0;
}

Anonymous Structs and Unions

#include <stdio.h>

// Anonymous union inside struct (C11)
struct Vector3D {
    union {
        struct { float x, y, z; };  // Anonymous struct
        float components[3];
    };
};

struct Message {
    int type;
    union {
        struct { int error_code; char error_msg[64]; };  // Error
        struct { float x, y; };                          // Position
        char raw_data[128];                              // Raw bytes
    };
};

int main(void) {
    struct Vector3D v = {.x = 1.0f, .y = 2.0f, .z = 3.0f};
    
    // Access both ways
    printf("v.x = %f\n", v.x);
    printf("v.components[0] = %f\n", v.components[0]);
    
    struct Message msg;
    msg.type = 1;
    msg.error_code = 404;
    strcpy(msg.error_msg, "Not Found");
    
    return 0;
}

C17 (C18) Features

C17 was primarily a bug-fix release with no major new features. It clarified ambiguities in C11.
// C17 mainly clarified:
// - __has_include preprocessor operator behavior
// - Atomic operations edge cases
// - Unicode string literal handling
// - Various undefined behavior specifications

// __has_include (standardized behavior)
#if __has_include(<optional_header.h>)
#include <optional_header.h>
#define HAVE_OPTIONAL 1
#else
#define HAVE_OPTIONAL 0
#endif

C23 Features

typeof and typeof_unqual

#include <stdio.h>

// typeof gives you the type of an expression
int main(void) {
    int x = 42;
    typeof(x) y = 100;        // y is int
    typeof(x + 1.0) z = 3.14; // z is double (due to promotion)
    
    // typeof_unqual removes const/volatile
    const int ci = 10;
    typeof(ci) a = 20;        // a is const int
    typeof_unqual(ci) b = 30; // b is int (non-const)
    
    // Useful in macros
    #define max(a, b) ({           \
        typeof(a) _a = (a);        \
        typeof(b) _b = (b);        \
        _a > _b ? _a : _b;         \
    })
    
    // Type-safe swap macro
    #define swap(a, b) do {        \
        typeof(a) _tmp = (a);      \
        (a) = (b);                 \
        (b) = _tmp;                \
    } while(0)
    
    int p = 1, q = 2;
    swap(p, q);
    printf("p=%d, q=%d\n", p, q);  // p=2, q=1
    
    return 0;
}

nullptr - Null Pointer Constant

#include <stdio.h>

// Before C23: NULL could be 0 or (void*)0
// Problem: 0 is also a valid int!

void func_int(int x) { printf("int: %d\n", x); }
void func_ptr(void *p) { printf("ptr: %p\n", p); }

// With NULL (problematic)
// func_overload(NULL);  // Which one is called? Depends on NULL definition!

// C23: nullptr is always a null pointer
// nullptr has type nullptr_t (a pointer type, never an integer)

int main(void) {
    int *p = nullptr;  // Clear: this is a null pointer
    
    if (p == nullptr) {
        printf("p is null\n");
    }
    
    // nullptr cannot be implicitly converted to int
    // int x = nullptr;  // ERROR in C23
    
    return 0;
}

constexpr - Compile-Time Constants

#include <stdio.h>

// constexpr ensures compile-time evaluation
constexpr int BUFFER_SIZE = 1024;
constexpr double PI = 3.14159265358979323846;

// Array sizes must be compile-time constants
char buffer[BUFFER_SIZE];  // OK with constexpr

// constexpr functions (limited in C23)
constexpr int square(int x) {
    return x * x;
}

// Use in array dimensions
int array[square(10)];  // 100 elements

// constexpr compound literals
constexpr int primes[] = {2, 3, 5, 7, 11, 13};

int main(void) {
    // constexpr local variables
    constexpr int local_const = 42;
    
    // Must be initialized with constant expression
    // constexpr int bad = rand();  // ERROR: not a constant expression
    
    return 0;
}

auto Type Inference

#include <stdio.h>

// C23 auto infers type from initializer
int main(void) {
    auto x = 42;          // x is int
    auto y = 3.14;        // y is double
    auto z = "hello";     // z is const char*
    
    // Works with complex types
    int arr[] = {1, 2, 3, 4, 5};
    auto ptr = arr;       // ptr is int*
    
    // Useful for long type names
    struct VeryLongTypeName { int a, b, c; };
    struct VeryLongTypeName obj = {1, 2, 3};
    auto p = &obj;        // p is struct VeryLongTypeName*
    
    // With typeof for consistency
    int original = 100;
    auto copy = original; // copy is int
    
    return 0;
}

[[attributes]] - Standard Attributes

#include <stdio.h>
#include <stdlib.h>

// [[nodiscard]] - Warn if return value is ignored
[[nodiscard]] int allocate_resource(void) {
    return 1;  // Returns handle that must be used
}

// [[maybe_unused]] - Suppress unused warnings
void debug_function([[maybe_unused]] int debug_level) {
    #ifdef DEBUG
    printf("Debug level: %d\n", debug_level);
    #endif
}

// [[deprecated]] - Mark as deprecated
[[deprecated("Use new_api() instead")]]
void old_api(void) {
    // ...
}

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

// [[fallthrough]] - Intentional switch fallthrough
void process(int state) {
    switch (state) {
        case 0:
            printf("Initializing\n");
            [[fallthrough]];  // Intentional, no warning
        case 1:
            printf("Running\n");
            break;
        case 2:
            printf("Stopping\n");
            break;
    }
}

// [[reproducible]] and [[unsequenced]] - Optimization hints (C23)
// [[reproducible]] - Pure function (no side effects, deterministic)
[[reproducible]] int pure_add(int a, int b) {
    return a + b;
}

int main(void) {
    allocate_resource();  // Warning: ignoring return value of nodiscard function
    
    old_api();  // Warning: 'old_api' is deprecated
    
    return 0;
}

Improved Enums

#include <stdio.h>

// C23: Specify underlying type for enums
enum Color : unsigned char {
    RED = 0,
    GREEN = 1,
    BLUE = 2,
    MAX_COLOR = 255
};

enum LargeEnum : long long {
    BIG_VALUE = 9223372036854775807LL
};

// Enum forward declarations with type
enum Status : int;  // Forward declare

void process_status(enum Status s);

enum Status : int {
    STATUS_OK = 0,
    STATUS_ERROR = -1,
    STATUS_PENDING = 1
};

int main(void) {
    printf("sizeof(Color) = %zu\n", sizeof(enum Color));  // 1
    printf("sizeof(LargeEnum) = %zu\n", sizeof(enum LargeEnum));  // 8
    
    return 0;
}

Binary Literals and Digit Separators

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

int main(void) {
    // Binary literals (already in GCC/Clang, now standard)
    int flags = 0b10110100;
    uint8_t mask = 0b1111'0000;  // With digit separator
    
    // Digit separators for readability
    long population = 7'900'000'000;
    double avogadro = 6.022'140'76e23;
    int permissions = 0b111'101'101;  // rwxr-xr-x
    
    // Hex with separators
    uint32_t color = 0xFF'80'00'FF;  // RGBA
    uint64_t address = 0x0000'7FFF'FFFF'0000;
    
    printf("flags = %d (0b%08b)\n", flags, flags);
    printf("population = %ld\n", population);
    
    return 0;
}

#embed - Binary File Inclusion

#include <stdio.h>

// C23: Embed binary files directly
// Replaces ugly xxd or bin2c hacks

// Embed a small icon file
static const unsigned char icon[] = {
    #embed "icon.png"
};

// With limit
static const unsigned char first_1k[] = {
    #embed "large_file.bin" limit(1024)
};

// With prefix/suffix
static const unsigned char data[] = {
    #embed "data.bin" prefix(0xAA, 0xBB,) suffix(, 0xCC, 0xDD)
};

// Check if file exists at compile time
#if __has_embed("optional_data.bin")
static const unsigned char optional[] = {
    #embed "optional_data.bin"
};
#define HAS_OPTIONAL_DATA 1
#else
#define HAS_OPTIONAL_DATA 0
#endif

int main(void) {
    printf("Icon size: %zu bytes\n", sizeof(icon));
    return 0;
}

Empty Initializer {}

#include <stdio.h>

// C23: Empty braces zero-initialize any type

int main(void) {
    // Zero-initialize variables
    int x = {};           // x = 0
    double d = {};        // d = 0.0
    void *p = {};         // p = NULL
    
    // Zero-initialize structs
    struct Point { int x, y, z; };
    struct Point origin = {};  // All members zero
    
    // Zero-initialize arrays
    int arr[100] = {};    // All elements zero
    
    // In function calls
    struct Config {
        int timeout;
        int retries;
        char *server;
    };
    
    void init_config(struct Config cfg);
    init_config((struct Config){});  // Pass zero-initialized config
    
    return 0;
}

Compatibility and Detection

Feature Detection Macros

#include <stdio.h>

// Standard version macro
#if __STDC_VERSION__ >= 202311L
    #define C23_AVAILABLE 1
#elif __STDC_VERSION__ >= 201710L
    #define C17_AVAILABLE 1
#elif __STDC_VERSION__ >= 201112L
    #define C11_AVAILABLE 1
#endif

// Feature test macros
#ifdef __STDC_NO_ATOMICS__
    // Atomics not available
#endif

#ifdef __STDC_NO_THREADS__
    // C11 threads not available
#endif

#ifdef __STDC_NO_VLA__
    // VLAs not available
#endif

// Compiler-specific feature detection
#ifdef __has_feature
    #if __has_feature(c_atomic)
        // Has atomics
    #endif
#endif

#ifdef __has_extension
    #if __has_extension(c_static_assert)
        // Has static assert
    #endif
#endif

// Header availability
#if __has_include(<threads.h>)
    #include <threads.h>
    #define HAVE_C11_THREADS 1
#else
    #include <pthread.h>
    #define HAVE_C11_THREADS 0
#endif

int main(void) {
    printf("C Standard: %ld\n", __STDC_VERSION__);
    return 0;
}

Version-Specific Code

// Portable static_assert
#if __STDC_VERSION__ >= 202311L
    // C23: Can omit message
    #define STATIC_ASSERT(expr) static_assert(expr)
#elif __STDC_VERSION__ >= 201112L
    // C11: Requires message
    #define STATIC_ASSERT(expr) _Static_assert(expr, #expr)
#else
    // Pre-C11: Compile-time trick
    #define STATIC_ASSERT(expr) \
        typedef char static_assertion_##__LINE__[(expr) ? 1 : -1]
#endif

// Portable nullptr
#if __STDC_VERSION__ >= 202311L
    // C23 has nullptr
#else
    #define nullptr ((void*)0)
#endif

// Portable typeof
#if __STDC_VERSION__ >= 202311L
    // C23 has typeof
#elif defined(__GNUC__)
    #define typeof __typeof__
#endif

// Portable attributes
#if __STDC_VERSION__ >= 202311L
    #define NODISCARD [[nodiscard]]
    #define DEPRECATED [[deprecated]]
#elif defined(__GNUC__)
    #define NODISCARD __attribute__((warn_unused_result))
    #define DEPRECATED __attribute__((deprecated))
#else
    #define NODISCARD
    #define DEPRECATED
#endif

Best Practices

Use Modern Features

Embrace _Static_assert, _Generic, and atomics. They prevent bugs at compile time.

Target C11 Minimum

C11 is widely supported. Use it as your baseline for new code.

Feature Detection

Use __STDC_VERSION__ and __has_include for portable code.

Document Standard

Always specify which C standard your project requires in documentation.

Exercises

Use _Generic to create a type-safe dynamic array that works with multiple types:
// Goal: Create macros that work like this:
vec_int *vi = vec_create(int);
vec_push(vi, 42);
int val = vec_get(vi, 0);

vec_double *vd = vec_create(double);
vec_push(vd, 3.14);
double dval = vec_get(vd, 0);
Implement a lock-free stack using C11 atomics:
struct Node {
    int value;
    _Atomic(struct Node *) next;
};

_Atomic(struct Node *) stack_top = NULL;

void push(int value);
bool pop(int *value);