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
Copy
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
Copy
#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
Copy
#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
Copy
#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>
Copy
#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>)
Copy
#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
Copy
#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.Copy
// 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
Copy
#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
Copy
#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
Copy
#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
Copy
#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
Copy
#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
Copy
#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
Copy
#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
Copy
#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 {}
Copy
#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
Copy
#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
Copy
// 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
Exercise 1: Type-Safe Container
Exercise 1: Type-Safe Container
Use
_Generic to create a type-safe dynamic array that works with multiple types:Copy
// 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);
Exercise 2: Lock-Free Stack
Exercise 2: Lock-Free Stack
Implement a lock-free stack using C11 atomics:
Copy
struct Node {
int value;
_Atomic(struct Node *) next;
};
_Atomic(struct Node *) stack_top = NULL;
void push(int value);
bool pop(int *value);