~/blabbing $ man 7 style-guide

STYLE-GUIDE(7)hachem.wtf manualSTYLE-GUIDE(7)

name

style guide - how i write code

synopsis

style-guide [--small-and-self-reliant] [--formatting] [--naming] [--types] [--prototypes-and-returns] [--comments] [--functions] [--memory-and-ownership] [--bounds-and-lengths] [--handle-every-case] [--fail-loud] [--build-clean] [--c] [--other-languages] [--a-full-example] [--the-configs]

description

how i write c and c++ in any project, plus the habits behind both. i wrote it down to stay consistent and stop re-deciding small things.

clang-format and clang-tidy enforce the formatting and naming, so drift fails the build instead of a review.

small and self-reliant

write the least code that solves the problem, behind the smallest interface. a big system is a burden even if you wrote it.

depend on as little as possible. a library swaps a problem you understand for one you can't see into or fix. if a dependency isn't clearly worth it, write the thing yourself. it's usually smaller than the integration, and yours to fix.

clarity beats cleverness. code is read far more than written, so write the obvious version over the one that saves a line. clever code costs the next reader, usually me.

formatting

braces go on their own line, for functions and blocks.

int main(void)
{
    if (ready)
    {
        (void)run();
    }
}

that gives every scope a clear top and bottom, which is worth the extra lines.

lines stop at 100 columns. declarations get one space after the type and no alignment. lining up a static size_t and a bare int leaves an ugly gap, and clang-format undoes it anyway.

the pointer binds to the type (int* p, not int *p), since pointer-to-int is one type. int* a, b only makes a a pointer, so declare one pointer per line.

a space follows a control keyword (if (, while () but never comes before a call's parenthesis (run()), so keywords don't look like calls.

no function, if, loop, or block goes on one line.

each case in a switch is a braced block with the break on the closing brace. the braces stop declarations leaking into the next case, and } break; ties the exit to its block. case labels sit flush with the switch braces.

switch (input[0])
{
case 'p':
{
    *(volatile char*)0 = 0;
} break;
case 'q':
{
    running = false;
} break;
default:
{
    puts("nothing");
} break;
}

the joined } break; is the only layout clang-format won't keep. it splits the brace and the break apart on every run. i keep it anyway, since the break then reads as closing the case instead of a loose statement. where the formatter runs over a switch, wrap it in a // clang-format off block.

naming

functions and parameters are lower_case. macros and compile-time constants are UPPER_CASE, so preprocessor substitutions look like what they are.

a variable's prefix shows its reach. a global with external linkage gets g_, a file-local static gets s_, and a struct field meant to stay internal gets m_. locals and parameters get none. every use of a name then shows where it lives and what should touch it.

c has no private, so m_ marks a field as not public, though anyone can still reach it. in c++ it goes on private members.

struct, enum, and union tags are CamelCase. enum constants are UPPER_CASE.

enum State
{
    IDLE,
    RUNNING,
    DONE
};

a library might add a short namespace prefix to its exported types, but that's outside the base style.

clang-tidy checks naming as warnings-as-errors, so a misnamed identifier fails the build, m_ on c++ private members included. i keep two things by hand. one is the g_/s_ split, since clang-tidy lumps file-scope variables together regardless of linkage. the other is m_ in c, which has no private members to check.

types

never typedef a struct or an enum. always write the tag.

struct Object* object;
enum State state;

a typedef hides what a name is. Object x; doesn't say whether Object is a struct, a pointer, or a scalar, and every use pays for that. struct Object says it for free.

size allocations from the variable, not the type name.

struct Object* object = malloc(sizeof *object);

sizeof *object ties the size to the thing being allocated. if object's type changes, the allocation does too, with no repeated type name to drift. sizeof takes an expression, so it needs no parentheses.

prototypes and returns

an empty parameter list is (void), never ().

void shutdown(void);

in c, () means unspecified arguments, not zero, so it quietly turns off argument checking. (void) says the function takes nothing.

cast discarded return values to (void), even for routine calls.

(void)printf("ready\n");

the cast tells a reader (and the compiler) you chose to ignore the result and didn't forget it.

comments

comment sparingly, and say why, not what. the code already says what, and a comment that repeats it goes stale. explain the reason for a decision, or a constraint the code can't show.

a function gets a one-line description only if its name and signature leave its behaviour unclear.

the exception is a path skipped on purpose, like a default you never expect to hit or an error you choose not to handle. a short note (sometimes flippant) tells the reader it's no mistake.

functions

small, one job each. if a function needs a paragraph to explain, it's two functions.

prefer early returns to nesting. handle the unusual case and leave, so the main path stays at the left margin.

avoid goto. loops, early returns, and small functions do the same job more clearly. c's usual reason for it is unwinding several resources on exit. per-resource scopes or helpers handle that better, with no growing tail of cleanup to jump to.

memory and ownership

every allocation has one owner that frees it, once. aliases are fine, but only the owner frees. when a function takes over or hands back an allocation, make that obvious in the signature or say so plainly, so who frees what is never a guess.

release on every path. a function that acquires a resource frees it on every exit, and error paths leak most because they're the ones you forget.

with several resources live, give each its own scope or small helper so acquire and release stay paired. otherwise one long function repeats its frees at every return.

a freed pointer is dead. don't read it, free it again, or pass it on. if it stays in scope, set it to NULL so a later use faults loudly instead of quietly touching freed memory.

check a pointer that can be null once, where it comes in, before dereferencing it. don't repeat the check in every function after that.

bounds and lengths

a buffer pointer is incomplete without its length. pass (buffer, length) as a pair, and keep a size next to a stored pointer. prefer calls that take a length (memcpy, snprintf) to ones that run to a terminator or trust the caller.

only index and write where you can show you're in bounds. anything else is a bug waiting for the wrong input.

handle every case

a switch over an enum handles every value. a missing case bites when the enum grows, so let -Wswitch turn it into an error.

a default should be a deliberate choice, never a way to silence that check. one you never expect to reach gets a flippant note like any other skipped path.

fail loud

assert what has to be true, like preconditions and cases that should never happen. a violation then stops the program where it went wrong, before it corrupts state and crashes somewhere unrelated. that's far cheaper to debug than limping along.

keep asserts meaningful though. asserting something that can legitimately happen is a crash you wrote yourself.

build clean

build with -Wall -Wextra -Werror -Wpedantic, so warnings are on and fatal. a warning is the compiler noticing something you didn't, and ignorable warnings end up ignored.

develop under the address and undefined sanitizers. they catch memory and ub bugs that rereading won't.

don't lean on undefined behaviour, even when it works. working today doesn't make it defined, and the next compiler doesn't have to agree.

c++

c++ keeps the formatting (allman braces), naming, comment, and control-flow rules. two c-only habits go, since c++ has better options. the (void) parameter list isn't needed, and manual malloc with sizeof *p gives way to the ownership rules below.

no exceptions

build with -fno-exceptions. exceptions are hidden control flow. any call might unwind, and nothing shows where a failure lands. they also cost binary size and predictability, which systems code shouldn't pay for without knowing.

return errors instead, as a status, an std::optional for value-or-nothing, or an std::expected for value-or-error. as in c, a failure should be a value you can see and must handle.

ownership through smart pointers

keep ownership in the type with smart pointers. use std::unique_ptr for a single owner, moved with std::move, and std::shared_ptr only for shared ownership. a raw pointer then means "i don't own this". it's the c rule that only the owner frees, enforced by the type instead of discipline.

auto make_widget() -> std::unique_ptr<Widget>;   // the caller owns the result

seal any manual memory management in one class that acquires in its constructor and releases in its destructor, so nothing outside touches the raw allocation. that gives it one owner and one place to get right, and cleanup runs on every way out, even after an early return.

trailing return types

default to the trailing form, auto name(args) -> type.

auto parse(std::string_view text) -> std::optional<Config>;

the name always sits right after auto, so signatures line up however long the return type is. it also works when the return type depends on the parameters. there's rarely a reason to use the leading form.

other languages

c and c++ leave surface style to the author, so those rules are mine. most languages don't. if a language ships its own formatter, naming scheme, or brace style, use those. go has gofmt and its own casing, rust has rustfmt and the api guidelines, and zig has zig fmt. write like the community does, and let the formatter win every argument.

the reason is the people reading it. code that looks like its ecosystem lets newcomers pick it up without learning a personal dialect, which is most of what conventions are for. fighting gofmt for allman braces makes code look foreign and helps no one.

python is loose and enforces nothing, so nothing clashes. match what the project uses (pep 8, or black if it leans that way). otherwise the general habits carry over.

bracing and casing belong to the language. the habits underneath come with you. keep it small, keep failures visible, own memory clearly when the language leaves that to you, and fail loud.

a full example

here's a small module using most of the rules, including the formatting, naming, and g_/s_/m_ prefixes, ownership released on every path, a braced switch, an assertion, and the literal sugar. comments only appear where the reason isn't on the line, so the obvious ring_free gets none.

/* ring.c, a fixed-capacity integer ring buffer with a small command
 * dispatch, gathered into one file to show the conventions together. */

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

#define RING_CAP  1_024              /* a power of two, so WRAP_MASK works */
#define WRAP_MASK 0b0011_1111_1111   /* RING_CAP - 1 */

/* external: callers may read it after a failure */
int g_last_error;

/* file-local tally, part of no interface */
static size_t s_push_count;

enum Command
{
    COMMAND_PUSH,
    COMMAND_DROP,
    COMMAND_QUIT
};

struct Ring
{
    int*   m_data;   /* internal: reached only through the ring_ functions */
    size_t m_head;
    size_t m_len;
};

/* allocates a ring and its buffer; the caller owns the result and releases
   it with ring_free. returns NULL if either allocation fails. */
struct Ring* ring_new(void)
{
    struct Ring* ring = malloc(sizeof *ring);
    if (!ring)
        return NULL;

    ring->m_data = malloc(RING_CAP * sizeof *ring->m_data);
    if (!ring->m_data)
    {
        free(ring);   /* release on the error path, not just the happy one */
        return NULL;
    }

    ring->m_head = 0;
    ring->m_len  = 0;
    return ring;
}

void ring_free(struct Ring* ring)
{
    if (!ring)
        return;

    free(ring->m_data);
    free(ring);
}

/* appends `count` values, overwriting the oldest once the ring is full */
static void ring_push(struct Ring* ring, const int* values, size_t count)
{
    assert(ring != NULL);   /* a null ring is a caller bug, so fail loud */

    for (size_t i = 0; i < count; i++)
    {
        size_t slot = (ring->m_head + ring->m_len) & WRAP_MASK;
        ring->m_data[slot] = values[i];

        if (ring->m_len < RING_CAP)
            ring->m_len++;
        else
            ring->m_head = (ring->m_head + 1) & WRAP_MASK;
    }

    s_push_count += count;
}

/* runs one command; returns false when the caller should stop the loop. */
static bool run_command(struct Ring* ring, enum Command command)
{
    switch (command)
    {
    case COMMAND_PUSH:
    {
        int value = 42;
        ring_push(ring, &value, 1);
    } break;
    case COMMAND_DROP:
    {
        if (ring->m_len > 0)
            ring->m_len--;
    } break;
    case COMMAND_QUIT:
    {
        return false;
    } break;
    default:
    {
        g_last_error = 1;
        (void)fprintf(stderr, "unknown command\n");   /* discard the count on purpose */
    } break;
    }

    return true;
}

int main(void)
{
    struct Ring* ring = ring_new();
    if (!ring)
        return 1;

    enum Command script[] = { COMMAND_PUSH, COMMAND_PUSH, COMMAND_DROP, COMMAND_QUIT };
    for (size_t i = 0; i < sizeof script / sizeof *script; i++)
    {
        if (!run_command(ring, script[i]))
            break;
    }

    (void)printf("pushed %zu\n", s_push_count);

    ring_free(ring);
    return 0;
}

the c++ version goes further. it returns errors instead of throwing them, keeps ownership in the type, uses trailing returns everywhere, and seals the one manual resource in a class.

class File
{
public:
    static auto open(const char* path) -> std::optional<File>;

    File(File&& other) noexcept;
    ~File();   // closes the handle it owns

    auto read_line() -> std::optional<std::string>;

private:
    explicit File(std::FILE* handle) : m_handle(handle) {}

    std::FILE* m_handle;   // the one manual resource, sealed inside this class
};

// the caller owns the buffer; move it to hand ownership on
auto make_scratch(std::size_t bytes) -> std::unique_ptr<std::byte[]>
{
    return std::make_unique<std::byte[]>(bytes);
}

octal literals and nested comments don't come up in this module, so the example skips them. both are under lexical conveniences in hecc.

the configs

this is the .clang-format.

Language: Cpp
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ContinuationIndentWidth: 8
ColumnLimit: 100
BreakBeforeBraces: Allman
PointerAlignment: Left
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: true
IndentCaseLabels: false
SpaceBeforeParens: ControlStatements
SortIncludes: false
Cpp11BracedListStyle: false
AlignOperands: DontAlign
AlignAfterOpenBracket: Align

and this is the .clang-tidy. a library adds its own StructPrefix, EnumPrefix, and macro-prefix exception on top.

Checks: '-*,readability-identifier-naming'
WarningsAsErrors: 'readability-identifier-naming'
CheckOptions:
  readability-identifier-naming.StructCase: CamelCase
  readability-identifier-naming.EnumCase: CamelCase
  readability-identifier-naming.EnumConstantCase: UPPER_CASE
  readability-identifier-naming.FunctionCase: lower_case
  readability-identifier-naming.ParameterCase: lower_case
  readability-identifier-naming.PrivateMemberPrefix: m_
  readability-identifier-naming.MacroDefinitionCase: UPPER_CASE

see also

hecc(7), hiv(7), iridis(7), blabbing(7), hachem(1)

author

hachem <im@hachem.wtf>

~/blabbing $