Modern C Features

repository·master·Indexed 20 days ago

https://github.com/anthonycalandra/modern-c-features

A collection of descriptions and examples for modern C language (C11, C17, C23) and library features. Covers C23 additions like auto type deduction, constexpr, nullptr, #embed, and attributes, as well as C11 features including atomic types, threads, bounds-checking functions, and generic selection.

Tokens
5.2K
Snippets
31
Records
32
Agent score
21%

What's inside modern-c-features

  1. Use atomic types and flags with `<stdatomic.h>`

    master

    C11 provides atomics to prevent data races.

    Atomic Types

    Use the _Atomic type specifier (or typedefs like atomic_int) to declare variables that are treated atomically. Ordinary read/write access to these types is sequentially consistent.

    Atomic Flags

    atomic_flag is a guaranteed lock-free, atomic boolean type. It is typically used for spinlocks via atomic_flag_test_and_set and atomic_flag_clear. Initialize them using ATOMIC_FLAG_INIT.

    Atomic Variables

    Atomic variables (declared with _Atomic) can be initialized using ATOMIC_VAR_INIT or atomic_init. Unlike flags, they are not guaranteed to be lock-free. They support advanced operations like atomic_compare_exchange_weak and atomic_store.

    // Example: Spinlock using atomic_flag
    struct spinlock
    {
        atomic_flag flag;
    };
    
    void acquire_spinlock(struct spinlock* lock)
    {
        while (atomic_flag_test_and_set(&lock->flag) == true);
    }
    
    void release_spinlock(struct spinlock* lock)
    {
        atomic_flag_clear(&lock->flag);
    }
    
    // Example: Spinlock using atomic_bool
    struct spinlock_bool
    {
        atomic_bool flag;
    };
    
    void acquire_spinlock_bool(struct spinlock_bool* lock)
    {
        bool expected = false;
        while (atomic_compare_exchange_weak(&lock->flag, &expected, true) == false)
        {
            expected = false;
        }
    }
    
    void release_spinlock_bool(struct spinlock_bool* lock)
    {
        atomic_store(&lock->flag, false);
    }
  2. Use Generic Selection with _Generic

    master

    The _Generic keyword allows you to select an expression based on the type of a controlling expression. This is useful for creating type-generic macros that behave differently depending on whether they receive an int, float, long, etc. The syntax is _Generic( controlling-expression, T1: E1, ... ). You can also use a default keyword to match any type not explicitly listed.

    #define abs(expr) _Generic((expr), \    int: abs(expr), \    long int: labs(expr), \    float: fabs(expr), \    /* ... */ \    /* Don't call abs for unsigned types, etc. */ \    default: expr \)
    
    printf("%d %li %f %d\n", abs(-123), abs(-123l), abs(-3.14f), abs(123u));
    // prints: 123 123 3.14 99 123
  3. Use anonymous structs and unions

    master

    Anonymous structs and unions allow you to define unnamed members within a parent struct or union. Every member of an anonymous union is treated as a direct member of the enclosing struct or union, allowing for flatter access patterns. This behavior applies recursively.

    struct v
    {
       union // anonymous union
       {
           int a;
           long b;
       };
       int c;
    } v;
     
    v.a = 1;
    v.b = 2;
    v.c = 3;
    
    printf("%d %ld %d", v.a, v.b, v.c); // prints "2 2 3"
    
    union v_alt
    {
       struct // anonymous struct
       {
           int a;
           long b;
       };
       int c;
    } v_alt;
     
    v_alt.a = 1;
    v_alt.b = 2;
    v_alt.c = 3;
    
    printf("%d %ld %d", v_alt.a, v_alt.b, v_alt.c); // prints "3 2 3"
  4. Use threads, mutexes, and condition variables with `<threads.h>`

    master

    The <threads.h> library provides OS-agnostic concurrency primitives.

    Threads

    Use thrd_create to spawn a new thread and thrd_join to wait for its completion.

    Mutexes

    Use mtx_t to protect critical sections. Mutexes can be initialized with different types via mtx_init:

    • mtx_plain: Standard mutex.
    • mtx_timed: Supports timed locking.
    • mtx_recursive: Supports recursive locking.

    Always clean up mutexes with mtx_destroy.

    Condition Variables

    Use cnd_t to allow threads to wait for specific conditions. Use cnd_wait to block and cnd_signal or cnd_broadcast to wake threads. Note that spurious wakeups can occur, so always check the condition in a loop.

    Always clean up condition variables with cnd_destroy.

    // Thread creation
    thrd_t thr;
    const int ret = thrd_create(&thr, (thrd_start_t) &print_n, (void*) &n);
    thrd_join(thr, NULL);
    
    // Mutex usage
    mtx_t mutex;
    mtx_init(&mutex, mtx_plain);
    mtx_lock(&mutex);
    // Critical section
    mtx_unlock(&mutex);
    mtx_destroy(&mutex);
    
    // Condition variable usage
    cnd_t cond;
    cnd_init(&cond);
    // In consumer thread:
    while (!can_consume && cnd_wait(&cond, &mutex));
    // In producer thread:
    can_consume = true;
    cnd_signal(&cond);
    cnd_destroy(&cond);
  5. Use bounds-checking library functions

    master

    C11 introduced bounds-checked versions of several standard library functions, identified by an _s suffix (e.g., fopen_s, gets_s, asctime_s).

    When a bounds check fails, the program invokes a constraint handler. You can manage this behavior using set_constraint_handler_s.

    Standard handlers include:

    • abort_handler_s: Writes to stderr and terminates the program.
    • ignore_handler_s: Ignores the violation and continues execution.

    You can also define a custom handler to implement specific error logic.

    void custom_handler_s(const char* restrict msg, void* restrict ptr, errno_t error)
    {
        fprintf(stderr, "ERROR: %s\n", msg);
        abort();
    }
    
    set_constraint_handler_s(custom_handler_s);
    char buffer[BUFFER_SIZE];
    gets_s(buffer, BUFFER_SIZE);
    printf("Entered: %s\n", buffer);
  6. Specify non-returning functions with noreturn

    master

    The noreturn keyword (or the noreturn macro from <stdnoreturn.h>) specifies that a function does not return to its caller. If the function attempts to return via a return statement or by reaching the end of the function body, behavior is undefined. This is typically used for functions that call exit() or enter infinite loops.

    noreturn void foo()
    {
        exit(0);
    }
  7. Create wide string literals

    master

    C11 supports creating 16-bit or 32-bit wide string literals and character constants using the u and U prefixes.

    • u prefix: char16_t (16-bit wide)
    • U prefix: char32_t (32-bit wide)
    char16_t c1 = u'貓';
    char32_t c2 = U'🍌';
    
    char16_t s1[] = u"a猫🍌"; // => [0x0061, 0x732B, 0xD83C, 0xDF4C, 0x0000]
    char32_t s2[] = U"a猫🍌"; // => [0x00000061, 0x0000732B, 0x0001F34C, 0x00000000]
  8. Open files in exclusive mode

    master

    When using fopen, you can append the x flag to the w or w+ mode specifiers. This creates an exclusive mode opening: the function will fail if the file already exists, preventing accidental overwriting.

    FILE* fp = fopen(fname, "w+x");
    if (!fp) {
        // File either exists or there was an error
    } else {
        fclose(fp);
    }
  9. Perform compile-time assertions with static_assert

    master

    Use _Static_assert or the static_assert macro (from assert.h) to perform assertions at compile-time. If the condition is false, the compiler will generate an error with the provided message. This is useful for verifying assumptions about type sizes or configuration constants.

    static_assert(sizeof(int) == sizeof(char), "`int` and `char` sizes do not match!");