style guide

how i write c and c++, plus the general habits underneath both. none of it is tied to a particular project. i wrote it down so it stays consistent and so i stop re-deciding the same small things over and over.

the formatting and naming rules aren't suggestions. clang-format and clang-tidy enforce them, with naming as warnings-as-errors, so code that drifts fails the build instead of a review. both configs are at the end.

small and self-reliant

keep things small. write the least code that solves the problem and the smallest interface that exposes it. a big system you wrote yourself is still a big system you have to carry.

depend on as little as possible. pulling in a library trades a problem you understand for one you don't, and you can't fix a stack you can't see into. if a dependency isn't clearly worth it, write the thing yourself. it usually ends up smaller than the integration would have been, and it's yours to fix when it breaks.

clarity beats cleverness. code gets read way more often than it gets written, so the version that's obvious at a glance beats the one that saves a line. clever code is a cost paid by whoever reads it next, and that's usually me.

formatting

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

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

a brace on its own line gives every scope a clear top and bottom edge, and that's worth more than the vertical space it costs.

lines stop at a hundred columns. declarations aren't aligned into columns, and a single space follows the type. lining up names across a static size_t and a bare int opens a gap big enough to be an eyesore on its own, and clang-format collapses that alignment anyway.

the pointer binds to the type, so it's int* p and not int *p. pointer-to-int is one type and should read like one. the catch is that int* a, b only makes a a pointer, so declare one pointer per line and the problem never comes up.

a space goes after a control keyword (if (, while () but never before a call's parenthesis (run()), so a keyword never looks like a function call.

nothing collapses onto one line. not a function, not an if, not a loop, not a block.

each case in a switch is its own braced block, with the break on the closing brace. the braces give the case its own scope, so a declaration in one case can't leak into the next, and } break; keeps the exit attached to the block it closes. case labels sit flush with the switch braces instead of being indented.

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

the joined } break; is the one bit of layout clang-format won't keep. it splits the brace and the break onto separate lines every time it runs. i do it anyway, because the break reads as part of closing the case instead of a statement floating after it. where the formatter runs over a switch, wrap it in a // clang-format off block so the joined form survives.

naming

functions and parameters are lower_case. macros and compile-time constants are UPPER_CASE, so a name that's really a preprocessor substitution looks like one wherever it's used.

a variable's prefix shows how far it reaches. a global with external linkage gets g_, a file-local static gets s_, and a struct field that's meant to stay internal gets m_. that way every use of a name tells you where it lives and what's supposed to touch it, not just the declaration. plain locals and parameters get no prefix. c has no private, so m_ is the signal that a field isn't part of the type's public surface, even though the language will happily let anyone reach it. it's a convention standing in for access control. in c++ the same prefix goes on members that really are private.

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 own exported types on top of this, but that's the library's rule and not part of the base style. most of this is enforced. clang-tidy checks naming with warnings-as-errors, so a misnamed identifier is a failed build, and that includes m_ on a c++ private member. two things it can't see are kept by hand. one is the g_/s_ split, since clang-tidy lumps every file-scope variable into one category no matter its linkage. the other is m_ in c, since c has no private members for it to recognise. those two are conventions the reader honours, not the compiler.

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 tell you whether Object is a struct, a pointer, or a scalar, and you pay for that at every use. writing struct Object keeps the kind visible for free.

allocate against 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 follows along on its own, and the type name is never written twice where it could drift out of sync. sizeof takes an expression, so it doesn't need parentheses here.

prototypes and returns

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

void shutdown(void);

in c, () means an unspecified argument list, not zero arguments, so it quietly switches off argument checking. (void) is the form that actually says the function takes nothing.

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

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

the cast marks the discard as deliberate, so a reader (and the compiler) can tell you chose to ignore the result instead of forgetting to check it. it's the same instinct as flagging an ignored branch in a comment, just moved into the code.

comments

comment sparingly, and say why, not what. the code already says what it does, and a comment that repeats it just goes stale. comments are for the reason behind a decision, or for a constraint you can't see in the code.

a function only gets a one-line description when its behaviour isn't clear from its name and signature. obvious functions get nothing.

the exception is a path you're ignoring on purpose, like a default you never expect to hit or an error you're choosing not to handle. a short note (sometimes a flippant one) saying the omission is intentional beats leaving the reader to wonder whether it was a mistake.

functions

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

prefer an early return over nesting. handle the exceptional case and leave, so the main path stays against the left margin instead of drifting right under each check.

avoid goto. structured control flow (loops, early returns, small functions) says the same things and is easier to follow. the one place c traditionally reaches for it is unwinding several resources on the way out, and that's better handled by keeping each resource's lifetime inside its own scope or helper, so there's no growing tail of cleanup to jump to.

memory and ownership

every allocation has exactly one owner, the one piece of code responsible for freeing it. aliases are fine and expected, but only the owner frees, and it frees once. when ownership moves, because a function takes over an allocation or hands one back, make that the obvious reading of the signature. where it isn't obvious, 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 the error paths are the ones that leak because they're the ones you forget. when several resources are live at once, keep each one's lifetime inside its own scope or small helper so its acquire and release stay paired. otherwise one long function piles up a set of frees it has to repeat at every return.

a freed pointer is dead. don't read it, free it again, or pass it on. if a freed pointer 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 before dereferencing it, and do it once, at the boundary where the value comes in. don't scatter the same defensive check through every function that touches it later.

bounds and lengths

a pointer to a buffer is incomplete without its length, so keep the two together. pass (buffer, length) as a pair, keep a size next to a stored pointer, and use the length-taking form of an operation (memcpy, snprintf) over the one that runs to a terminator or just trusts the caller. only index and write within the extent you can actually account for. an access you can't show is inside the buffer is a bug waiting for the wrong input.

handle every case

a switch over an enum handles every value. a missing case is a gap that shows up later when the enum grows, so let the compiler find it. -Wswitch turns an unhandled case into an error. a default is a deliberate choice and not a way to silence the check, and a default you never expect to reach gets the same flippant note as any other path you're choosing to ignore.

fail loud

assert what has to be true. a precondition, or a case that should never happen, is worth stating as an assertion so a violation stops the program right where it went wrong, instead of corrupting state and blowing up somewhere unrelated later. failing fast and loud is way cheaper to debug than limping along. keep the assertions meaningful though. an assert on something that can legitimately happen is just a crash you wrote for yourself.

build clean

build with the warnings on and fatal, so -Wall -Wextra -Werror -Wpedantic. a warning is the compiler noticing something you didn't, and a warning you're allowed to ignore is one you eventually will. run under the address and undefined sanitizers while developing, since they catch the memory and ub bugs no amount of rereading will find. and don't lean on undefined behaviour even when it happens to work. "works today" isn't "defined," and the next compiler doesn't have to agree.

c++

the formatting, naming, comment, and control-flow rules above carry over unchanged. that means allman braces, the naming scheme, sparse comments, and no goto. a couple of the c-only idioms go away since c++ has better options. the explicit (void) parameter list isn't needed, and manual malloc with sizeof *p gives way to the ownership rules below. everything after this is specific to c++.

no exceptions

no exceptions. build with -fno-exceptions. an exception is invisible control flow. any call might unwind, and where a failure ends up isn't written down anywhere. on top of that you get the binary size and the unpredictability, which systems code shouldn't be paying for without knowing it. errors get returned, not thrown, as a status, an std::optional for value-or-nothing, or an std::expected for value-or-error. a failure should be a value you can see and have to handle, exactly like in the c above.

ownership through smart pointers

keep ownership in the type. when something is owned or handed off, say so with a smart pointer. use std::unique_ptr for a single owner, moved with std::move to transfer it, and std::shared_ptr only where ownership really is shared. a raw pointer then means "i don't own this," a non-owning observer. it's the c rule that only the owner frees, except now the type holds it up instead of discipline.

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

where you actually need manual memory management, seal it inside one class. that class acquires in its constructor and releases in its destructor, and nothing outside it touches the raw allocation. so the manual part has one owner and one place to get right, and cleanup runs on every path out by construction, including the paths an early return would have leaked.

trailing return types

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

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

it puts the name in the same place every time, right after auto, so signatures line up and read the same whether the return type is one word or a long one. it's also the form that still works when the return type depends on the parameters. there's rarely a reason to go back to the leading type.

other languages

the c and c++ rules above are my own because those languages leave surface style up to the author. most languages don't. where a language ships its own conventions, like a formatter, a naming scheme, or a settled brace style, use those instead of mine. go has gofmt and its own casing, rust has rustfmt and the api guidelines, and zig has zig fmt. write each one the way its community writes it, and let the formatter win every argument.

the reason is the people reading it. code that looks like the ecosystem it lives in is code a newcomer from that ecosystem can pick up without learning a personal dialect first, and that's most of what a convention is for. fighting gofmt to keep allman braces just makes the code look foreign and doesn't help anyone.

python is the loose one. the language enforces nothing, so there's nothing to clash with. match whatever the project already uses (pep 8, or black if it leans that way), and otherwise the general habits carry over fine.

what comes with you into any language isn't the bracing or the casing. it's the habits underneath. keep it small, keep failures visible, own memory clearly when the language leaves that to you, and fail loud. the surface belongs to whatever language you're writing in.

a full example

here's a small module that uses most of the rules at once, including the formatting and naming, the g_/s_/m_ prefixes, ownership released on every path, a braced switch, an assertion, and the literal sugar. comments only show up where the reason isn't already on the line, and ring_free is obvious, so it 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 takes the same ideas further. errors are returned instead of thrown, ownership lives in the type, trailing returns are used everywhere, and the one manual resource is sealed 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);
}

the example doesn't force in every lexical nicety. octal literals and nested comments only come up in code this module doesn't have, and both are covered 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 of it.

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