A developer shares their personal style guide for writing C in the modern era, focusing on type safety, explicit error handling, and leveraging C23 features to create more robust systems while acknowledging the language's inherent limitations.
C remains a foundational language in computing, yet its evolution has been uneven. While newer languages bake in safety and ergonomics from the start, C's philosophy has always been to trust the programmer. This trust, however, often manifests as a minefield of undefined behavior and inconsistent practices. The author of this piece, a longtime C practitioner, navigates this landscape by adopting a personal style guide—a set of habits that impose modern, type-safe discipline on a language that offers little of it natively.
The foundation of this approach is a commitment to C23, the latest standard. While many C projects target older standards for maximum portability, the author argues that C23 provides crucial features that enable a safer coding style. A simple, assertive check ensures the environment meets a baseline assumption: #if CHAR_BIT != 8 followed by #error. This isn't just a portability guard; it's a declaration that the code is written for the modern world of 8-bit bytes, a reasonable constraint for most non-embedded systems.
Type clarity is the next pillar. Inspired by Rust's succinct integer types and the work of developers like Chris Wellon, the author employs a suite of typedefs: u8, i32, f64, and so on. This practice eliminates the ambiguity of standard types like int or long, whose sizes are platform-dependent. By making the exact size explicit, the code becomes self-documenting and less prone to overflow errors. The bool type from C23 (or stdbool.h for older standards) is preferred over integer flags, providing semantic clarity for truth values.
Perhaps the most significant departure from tradition is the treatment of strings. The null-terminated string, a source of countless buffer overflows and length calculation errors, is relegated to a legacy interface. The author instead uses a String struct containing a pointer and a length. This single change transforms string handling from a constant exercise in boundary checking to a more managed, predictable operation. It acknowledges the reality of interfacing with C's standard library while providing a safer internal representation.
The philosophical core of this style is the principle of "parse, don't validate." Instead of writing functions that accept raw data and perform checks at the point of use, the author advocates for creating strict, opaque types that can only be constructed via trusted parsing functions. A SafeBuffer type, for instance, might be created by a function that validates input and guarantees invariants. Once an object of this type exists, the rest of the code can assume its validity. This shifts error checking from a runtime burden to a compile-time contract, enforced by the type system as much as C allows.
C23's standardization of compatible tagged types with identical names and contents opens the door for tuples—a way to return multiple values from a function without the boilerplate of named structs. The author uses a macro to generate tuple types, like Tuple2(char*, int). However, this feature has sharp edges; it fails with pointer types due to token pasting issues, limiting its ergonomics. This is a classic C trade-off: a powerful feature that requires careful handling to avoid pitfalls.
For error handling, the author embraces sum types through disciplined struct design. A MaybeBuffer struct contains a union of a valid value and an error code, guarded by a boolean flag. This pattern, common in languages like Rust and Haskell, forces the caller to acknowledge the possibility of failure. While verbose compared to native Result types, it brings a measure of predictability to C's error-prone function returns. When combined with the "parse, don't validate" approach, these result types create a flow where errors are handled systematically rather than ignored.
Dynamic memory management is treated with caution. The author admits to avoiding it where possible, preferring to use languages like Rust or C# for heap-intensive tasks. When unavoidable, the trend toward arena allocators is noted as a promising direction for simplifying lifetime management, though it's not yet a personal staple.
Other habits include a preference for the mem* family of functions over str* functions for memory operations, and a general skepticism toward the standard library's higher-level APIs, often opting to reimplement them for better control. The emphasis is always on checking documentation and understanding the full implications of any non-trivial operation.
This personal style guide is not presented as a universal solution. It acknowledges that C's versatility means different contexts—embedded systems, high-performance computing, or kernel development—demand different approaches. Yet, for general-purpose programming, these habits represent a conscious effort to bring modern software engineering principles to a language that predates them. It's a testament to C's enduring power that such a style is even possible, and a reminder that even in a language of such freedom, discipline can be a powerful tool for creating reliable software.

Featured image: A stylized representation of C code, symbolizing the blend of legacy and modern practices.

Comments
Please log in or register to join the discussion