style guide
How I write C and C++, and the general habits under both. None of it is tied to a particular project; it is written down so it stays consistent and so I stop re-deciding the same small things.
The formatting and naming here are not suggestions. They are enforced by clang-format and clang-tidy, the naming as warnings-as-errors, so code that drifts fails the build instead of a review. The two configs are reproduced at the end.
Small and self-reliant
Keep things small. Prefer the least code that solves the problem and the smallest interface that exposes it. A large system you wrote yourself is still a large system to carry.
Depend on as little as possible. Pulling in a library trades a problem you understand for one you do not, and a stack you cannot see into is a stack you cannot fix. Where a dependency is not clearly worth it, write the piece yourself; it is usually smaller than the integration would have been, and it is yours to fix when it breaks.
Clarity beats cleverness. Code is read far more than it is written, so the version that is obvious at a glance beats the version that saves a line. Clever is a cost paid by whoever reads it next, and that is usually me.
Formatting
Braces 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, which is worth more than the vertical space it costs.
Lines stop at a hundred columns. Declarations are not aligned into columns; a single space follows the type. Lining names up across a static size_t and a bare int opens a gap wide enough to be its own eyesore, and clang-format collapses that alignment anyway.
The pointer binds to the type: int* p, not int *p. Pointer-to-int is a single type and should read as one. The cost is that int* a, b makes only a a pointer, so declare one pointer per line and the ambiguity never arises.
A space follows a control keyword (if (, while (), never a call's parenthesis (run()), so a keyword never looks like a function call.
Nothing collapses onto one line: not a function, an if, a loop, or a block.
Each case in a switch is a brace-enclosed block, with the break on the closing brace. The braces give the case its own scope, so a declaration in one case cannot leak into the next, and } break; keeps the exit tied to the block it closes. Case labels sit flush with the switch braces, not 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 piece of layout clang-format does not keep -- it splits the brace and the break onto separate lines on every run. It is deliberate anyway: the break reads as part of closing the case, not as a statement floating after it. Where the formatter runs across a switch, guard it with 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 is really a preprocessor substitution looks like one where it is used.
A variable's prefix marks its reach: a global with external linkage takes g_, a file-local static takes s_, and a struct field meant to stay internal takes m_, so a name shows where it lives, and what should touch it, at every use rather than only where it was declared. Plain locals and parameters take no prefix. C has no private, so m_ is the signal that a field is not part of the type's public surface even though the language will happily let anyone reach it -- a convention standing in for access control. In C++ the same prefix rides on genuinely private members.
Struct, enum, and union tags are CamelCase. Enum constants are UPPER_CASE:
enum State
{
IDLE,
RUNNING,
DONE
};
A given library may prefix its own exported types with a short namespace tag on top of this, but that is the library's rule, not part of the base style. Most of this is enforced: clang-tidy checks the naming with warnings-as-errors, so a misnamed identifier is a failed build, m_ on a C++ private member included. What it cannot see is kept by hand: the g_/s_ split, since it lumps every file-scope variable into one category regardless of linkage, and m_ in C, since C has no private members for it to recognise. Those two are conventions the reader honours rather than 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; tells you nothing about whether Object is a struct, a pointer, or a scalar, and you pay that at every use. Spelling 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, so if object's type changes the allocation follows on its own, and the type name is never written twice to drift out of sync. sizeof takes an expression, so it needs no 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 turns off argument checking. (void) is the form that actually says the function takes nothing.
Discarded return values are cast to (void), including 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 rather than forgot to check it. It is the same instinct as flagging a deliberately-ignored branch in a comment, moved into the code itself.
Comments
Comment sparingly, and say why, not what. The code already states what it does, and a comment that repeats it only goes stale. Comments are for the reason behind a decision, or a constraint that is not visible in the code itself.
A function gets a one-line descriptor only when its behaviour is not clear from its name and signature. Obvious functions get nothing.
The exception is the path you are deliberately ignoring: a default you never expect to hit, an error you are choosing not to handle. A short, sometimes flippant, note that the omission is on purpose is worth more than leaving the reader to wonder whether it was a mistake.
Functions
Small, one job each. A function that needs a paragraph to explain is two functions.
Prefer an early return to nesting. Handle the exceptional case and leave, so the main path stays against the left margin instead of drifting rightward under each check.
Avoid goto. Structured control flow -- loops, early returns, small functions -- says the same things and stays easier to follow. The one case C traditionally reaches for it, unwinding several resources on the way out, is better handled by keeping each resource's lifetime inside its own scope or helper, so there is no growing tail of cleanup to jump to.
Memory and ownership
Every allocation has exactly one owner: the single piece of code responsible for freeing it. Aliases are fine and expected, but only the owner frees, and it frees once. When ownership moves -- a function takes over an allocation, or hands one back -- make that the obvious reading of the signature, and say so plainly where it is not, so who frees what is never left to guesswork.
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 are 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, rather than letting one long function accumulate a set of frees it has to repeat at every return.
A freed pointer is dead: do not read it, free it again, or pass it on. Where 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 enters, rather than scattering the same defensive check through every function that later touches it.
Bounds and lengths
A pointer to a buffer is incomplete without its length, so carry the two together. Pass (buffer, length) as a pair, keep a size beside a stored pointer, and reach for the length-taking form of an operation -- memcpy, snprintf -- over the one that runs to a terminator or simply trusts the caller. Index and write only within the extent you can actually account for; an access you cannot 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 surfaces later as the enum grows, so let the compiler find it: -Wswitch turns an unhandled case into an error. A default is a deliberate choice, not a way to silence the check, and a default you never expect to reach earns the same flippant note as any other path you are choosing to ignore.
Fail loud
Assert what must be true. A precondition, or a case that should never happen, is worth stating as an assertion so a violation stops the program where it went wrong instead of corrupting state and surfacing somewhere unrelated later. Failing fast and loud is far cheaper to debug than limping on. Keep the assertions meaningful, though: an assert on something that can legitimately occur is just a crash you wrote for yourself.
Build clean
Build with the warnings on and fatal: -Wall -Wextra -Werror -Wpedantic. A warning is the compiler noticing something you did not, and a warning you are permitted to ignore is one you eventually will. Run under a sanitizer, address and undefined, while developing, where it catches the memory and UB bugs no amount of rereading finds. And do not lean on undefined behaviour even when it happens to work; "works today" is not "defined," and the next compiler is under no obligation to agree.
C++
The formatting, naming, comment, and control-flow rules above carry over unchanged: Allman braces, the naming scheme, sparse comments, no goto. A couple of the C-only idioms drop, since C++ has better -- the explicit (void) parameter list is unnecessary, and manual malloc with sizeof *p gives way to the ownership rules below. What follows 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 goes is written nowhere -- on top of binary size and unpredictability that systems code should not pay for silently. Errors are returned, not thrown: a status, an std::optional for value-or-nothing, an std::expected for value-or-error. A failure should be a value you can see and are made to handle, exactly as 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: std::unique_ptr for a single owner, moved with std::move to transfer it; std::shared_ptr only where ownership is genuinely shared. A raw pointer then means "I do not own this," a non-owning observer -- the C rule that only the owner frees, now held up by the type instead of by discipline.
auto make_widget() -> std::unique_ptr<Widget>; // the caller owns the result
Where manual memory management is actually needed, 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, one place to get right, and cleanup that runs on every path out by construction -- including the paths an early return would have leaked.
Trailing return types
Prefer the trailing form, auto name(args) -> type, as the default:
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 a single word or a long one, and it is the form that still works when the return type depends on the parameters. There is rarely a reason to fall back to the leading type.
Other languages
The C and C++ rules above are mine because those languages leave the surface style to the author. Most languages do not, and where a language ships its own conventions -- a formatter, a naming scheme, a settled brace style -- use them, not these. Go has gofmt and its own casing, Rust has rustfmt and the API guidelines, Zig has zig fmt. Write each the way its own community writes it, and let the formatter win every disagreement.
The reason is the people who read it. Code that looks like the ecosystem it lives in is code a newcomer from that ecosystem can pick up without first learning a personal dialect, and that is most of what a convention is for. Fighting gofmt to keep Allman braces marks the code as foreign and helps no one.
Python is the loose case: the language enforces nothing, so there is 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 travels with you in any language is not the bracing or the casing but the habits under them: keep it small, keep failures visible, own memory clearly where the language leaves that to you, fail loud. The surface belongs to whatever you are writing in.
A full example
A small module that leans on the rules at once: the formatting and naming, the g_/s_/m_ prefixes, ownership released on every path, a braced switch, an assertion, the literal sugar. Comments sit only where the reason is not already on the line, and ring_free, being obvious, carries 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++ rules take the same shape further -- errors returned rather than thrown, ownership carried in the type, trailing return everywhere, the one manual resource 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 does not contrive a use of every lexical nicety: octal literals and nested comments only come up in code this module does not have, and both are shown under Lexical Conveniences.
The configs
.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
.clang-tidy (a library adds its own StructPrefix, EnumPrefix, and macro-prefix exception on top of this):
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