C (Programming Language)
C is a compiled programming language created in the 1970s. The most common C compilers are gcc, clang & MSVC.
Programming Idioms
Macros: scoped
A begin-end macro that runs the first argument after entering the following scope and the second argument before exiting it.
#define token_paste(a, b) a##b #define concat(a,b) token_paste(a,b) #define macro_var(name) concat(name, __LINE__) #define scoped(start, end) \ int macro_var(_i_) = 0; \ for(start; !macro_var(_i_) != 0; (macro_var(_i_)++, end)) \ for(; !macro_var(_i_) != 0; macro_var(_i_)++) // usage scoped(printf("Hel"), printf("rld\n")) { printf("lo Wo"); break; // still runs end expression }
Here a version that only runs what's in the scope if the begin expression returns true (it will still evaluate the end expression, however):
#define DeferLoopChecked(begin, end) for(int _i_ = 2 * !(begin); (_i_ == 2 ? ((end), 0) : !_i_); _i_ += 1, (end)) /* usage */ DeferLoopChecked((0 == 1), printf("World\n")) { printf("Hello\n"); }
Macros: with
Statement similar to python's with
using macros:
#define token_paste(a, b) a##b #define concat(a,b) token_paste(a,b) #define macro_var(name) concat(name, __LINE__) #define with(declare, cleanup) \ int macro_var(_i_) = 0; \ for(declare; !macro_var(_i_) != 0; (macro_var(_i_)++, cleanup)) \ for(; !macro_var(_i_) != 0; macro_var(_i_)++) #define withif(declare, cond, cleanup) \ int macro_var(_i_) = 0; \ for(declare; !macro_var(_i_) != 0; (macro_var(_i_)++, (cond ? cleanup : (void)0))) \ if (cond) for(; !macro_var(_i_) != 0; macro_var(_i_)++) int main () { with (FILE *fp = fopen("c.org", "r"), fclose(fp)) { printf("with: %p\n", fp); break; // cleanup still runs // return; // cleanup will NOT run } withif (FILE *fp = fopen("c.org", "r"), fp != NULL, fclose(fp)) { // won't enter because fp == NULL printf("withif: %p\n", fp); } else { printf("withif: no\n"); } }
Take for example the SDL initialization boilerplate:
if (SDL_Init(SDL_INIT_VIDEO) < 0) { SDL_Log("Init error\n"); } SDL_Window* window = SDL_CreateWindow("window", 800, 600, 0); if (!window) { SDL_Log("Window error\n"); SDL_Quit(); } SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL); if (!renderer) { SDL_Log("Renderer error\n"); SDL_DestroyWindow(window); SDL_Quit(); } // main loop SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit();
Now with the withif
statement:
withif (int init = SDL_Init(SDL_INIT_VIDEO), init >= 0, SDL_Quit()) { withif (SDL_Window* window = SDL_CreateWindow("window", 800, 600, 0), window != NULL, SDL_DestroyWindow(window)) { withif (SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL), renderer != NULL, SDL_DestroyRenderer(renderer)) { // main loop } else { SDL_Log("Renderer error\n"); } } else { SDL_Log("Window error\n"); } } else { SDL_Log("Init error\n"); }
Macros: Comma-Operator in Expressions
Use comma operator to include e.g. an assertion in a macro that is supposed to expand to an expression:
#define GET_FIRST_ELEM(a) (assert(a->length > 0), a->array[0])
How to switch between game modes
How to easily switch between e.g. game modes:
b32 keep_running = false; do { switch(game_state->current_mode) { case GAME_MODE_MAIN_MENU: { keep_running = game_update_main_menu(game_state, input, game_state->title_screen); } break; case GAME_MODE_CUTSCENE: { keep_running = game_update_cutscene(game_state, input, game_state->cutscene); } break; case GAME_MODE_WORLD: { keep_running = game_update_world(game_state, input, game_state->world); } break; default: { UNREACHABLE("invalid game mode\n"); } break; } } while(keep_running);
A good way to add a discriminator to a discriminated union
Accessing the discriminator in a discriminated union without casting or having to specify an access qualifier:
/* library side */ typedef struct { int type, size; } mu_BaseCommand; typedef struct { mu_BaseCommand base; void *dst; } mu_JumpCommand; typedef struct { mu_BaseCommand base; mu_Rect rect; mu_Color color; } mu_RectCommand; typedef union { int type; mu_BaseCommand base; mu_JumpCommand jump; mu_RectCommand rect; /* ... */ } mu_Command; /* user side */ switch (cmd->type) { case RECT_COMMAND: /* ... */ break; case JUMP_COMMAND: /* ... */ break; // ... }
Arrays: Store 2D array in a 1D array
x 12345 ----- y height = 3 | | 1 width = 5 | X | 2 idx = 9 | | 3 ----- x = idx % width = 4 y = ceil(idx / width) = 2
Arrays: Cycle through a ring buffer
u8 ring_buf[RING_BUF_SIZE]; for (int i = 0; i < ENTRIES_TO_ADD; i++) { ring_buf[i % RING_BUF_SIZE] = entries[i]; }
Memory: Header and Data in a Single allocation
struct header_and_data_t { u64 cap; u64 len; u8* data; }; header_and_data_t = malloc(sizeof(header_and_data) + cap * sizeof(u8))
Memory: Handles instead of Pointers
From Handles are the better pointers:
- move memory management into centralized systems (rendering, physics, …)
- systems are the sole owner of their memory allocations
- group items of same type into arrays, treat base pointer as system-private
- creating an item only returns an 'index-handle' to the outside, not a pointer
- index-handles use as many bits as needed for the array index
- use remaining bits for additional memory safety checks
- convert a handle to a pointer when needed, but don't store pointer anywhere
Pseudo Type-Safety with void*
APIs
When a C API implements e.g. a generic data structure like a dynamic array or
hash table using void*
, all type information is stripped - thus no real type
safety can be given. However, by restraining the usage of the API to some
bespoke macros, we can still have a compile-time type check to avoid passing the
wrong type:
/* implementation */ void* grow(void* ptr, int* len, int* cap); /* defined in either usage or implementation code */ #define vec_push(V) ((V)->data = grow(&(V)->data, &(V)->len, &(V)->cap), &(V)->data[(V)->len++]) /* usage code */ *vec_push(v) = 5;
Strong Typing for typedef
typedef
by default only creates a weak type alias:
typedef int meters_t; typedef int hours_t; meters_t m = 1; hours_t h = m; // not an error
Wrapping the types into a struct makes the code properly typesafe:
typedef struct { int val; } meters_t; typedef struct { int val; } hours_t; meters_t m = { 1 }; hours_t h = m; // compile error
Writing out a string literal comfortably
Writing a glsl shader string or similar comfortably with a macro
#define SHADER_STR(x) "#version 330\n" #x // workaround to include strings starting with # const char* vs = SHADER_STR( uniform mat4 u_mvp; in vec2 in_pos; in vec2 in_uv; out vec2 v_uv; void main( ) { v_uv = in_uv; gl_Position = u_mvp * vec4(in_pos, 0, 1); } );
Struct: Default & named parameters
#include <stdio.h> typedef struct thing_t { int foo; float bar; size_t width; float zero_by_default; } thing_t; void default_and_named_args_(thing_t* thing) { printf("Width: %zu\n", thing->width); printf("Foo: %i\n", thing->foo); printf("Bar: %f\n", thing->bar); printf("Zero: %f\n", thing->zero_by_default); } #define default_and_named_args(...) \ default_and_named_args_(&(thing_t) { .width = 5, __VA_ARGS__ }) //#pragma GCC diagnostic push //#pragma GCC diagnostic ignored "-Winitializer-overrides" int main(void) { //thing_t thing = { .foo = 3, .bar = 0.141f }; //default_and_named_args(&thing); default_and_named_args(.bar = 1.1f, .foo = 4); // override default argument default_and_named_args(.width = 100, .foo = 22); return 0; } //#pragma GCC diagnostic pop
Struct: C/C++ initialization using ctor()
C and C++ differ in their syntax for initializing structs:
#ifdef __cplusplus #define ctor(TYPE, ...) (TYPE {__VA_ARGS__}) #else #define ctor(TYPE, ...) ((TYPE){__VA_ARGS__}) #endif typedef struct aabb_t { float min, max; } aabb_t; #define aabb_t(...) ctor(aabb_t, __VA_ARGS__)
Struct: Default Values using ctor()
Previous approach can be further used to introduce default values for members:
#ifdef __cplusplus #define ctor(TYPE, ...) (TYPE {__VA_ARGS__}) #else #define ctor(TYPE, ...) ((TYPE){__VA_ARGS__}) #endif typedef struct aabb_t { float min, max; } aabb_t; #define aabb_t(...) ctor(aabb_t, __VA_ARGS__) typedef struct line_t { float a, b; int foo; } line_t; #define line_t(...) ctor(line_t, .foo = 5, __VA_ARGS__) // default value *before* __VA_ARGS__ int main() { line_t line = line_t(); printf("%f %f %i\n", line.a, line.b, line.foo); line = line_t(.foo = 10); // override default value printf("%f %f %i\n", line.a, line.b, line.foo); }
This is useful to set nil struct pointers:
#ifdef __cplusplus #define ctor(TYPE, ...) (TYPE {__VA_ARGS__}) #else #define ctor(TYPE, ...) ((TYPE){__VA_ARGS__}) #endif typedef struct aabb_t { float min, max; } aabb_t; #define aabb_t(...) ctor(aabb_t, __VA_ARGS__) typedef struct entity_t { int transform; struct entity_t* next; } entity_t; entity_t nil_entity = ctor(entity_t, .transform = 0, .next = 0); #define entity_t(...) ctor(entity_t, .next = &nil_entity, __VA_ARGS__) int entity_is_nil(entity_t* entity) { return (entity == &nil_entity); } int main() { entity_t ent = entity_t(.next = 0); }
Caveats for this approach:
- C++ won't allow overriding the default member, since specifying a designated initializer multiple times is an error (g++, cl) or a warning (clang++)
Struct: Defining a "common data struct"
Use #define
anonymous structs to have a bundle of data at a centralized location
and easily add it to other structs without having to add an access qualifier
(like a C version of inheritance):
// in renderer.h #define RENDERER_COMMON_DATA \ struct \ { \ RendererRequest active_request; \ i32 flags; \ } // in opengl_renderer.h struct opengl_renderer_t { RENDERER_COMMON_DATA; // ... }; // in software_renderer.h struct software_renderer_t { RENDERER_COMMON_DATA; // ... }; // accessing the common data: opengl_renderer_t ogl_renderer = {}; ogl_renderer.active_request = ...; if (ogl_renderer.flags) ...
Compare this to:
// in renderer.h struct renderer_common_data_t { RendererRequest active_request; i32 flags; } // in opengl_renderer.h struct opengl_renderer { renderer_common_data_t common; // ... }; // in software_renderer.h struct software_renderer { renderer_common_data_t common; // ... }; // accessing opengl_renderer_t ogl_renderer = {}; ogl_renderer.common.active_request = ...; if (ogl_renderer.common.flags) ...
Shortcomings of C
- Header files in general
- The preprocessor in general
- No module system (
#include
is just a copy-paste) - Null-terminated strings
- Array to pointer decay
- No built-in dynamic array
- No built-in hashtable
- No multiple return values
- No (built-in) coroutines
- No defer statement
- Can't chain break statements or pass in number of breaks
- No introspection
- No built-in matrices or vectors
- Bad syntax for function pointers
- Ambiguous grammar makes parsing difficult
- No methods or Uniform Function Call Syntax (UFCS)
- No nested functions (only with GNU extensions)
Generics in C11
The _Generic
macro in C11 acts like a switch statement that switches on types:
float minf(float a, float b) { if (a > b) { return b; } else { return a; } } int mini(int a, int b) { if (a > b) { return b; } else { return a; } }; /* _Generic ( controlling-expression , association-list ) */ #define min(a,b) _Generic((a), float: minf(a,b), int: mini(a,b)) #define print_value(a) _Generic((a), float: printf("%f\n",a), int: printf("%i\n",a)) print_value(min(4.2f,2.8f)); print_value(min(3,8));
It can include a default case as a fallback. See Workarounds for C11 _Generic.
To check for support of _Generic
:
/* check based on compiler support */ #if ((__GNUC__*10000+__GNUC_MINOR__*100+__GNUC_PATCHLEVEL__)>=40900) || ((__clang_major__*10000+__clang_minor__*100+__clang_patchlevel__)>=30000) || (__xlC__>=0x1201) printf("Generic supported\n"); #endif /* check based on C11 standard */ #if __STDC__==1 && __STDC_VERSION >= 201112L #endif
X-macros can be used in combination with _Generic
:
#include <stdio.h> #define TYPES(X) \ X(int) \ X(float) \ #define TYPE_TO_STRING(type,...) type : serialize_##type, void serialize_int(int s) { printf("i: %i\n", s); } void serialize_float(float s) { printf("i: %f\n", s); } void dummy() {}; #define serialize(i) \ _Generic(i, \ TYPES(TYPE_TO_STRING) \ default : dummy)(i) int main() { int a = 1; float b = 2; serialize(a); serialize(b); }