Code Style Guide
Introduction
This page defines the code style and general guidelines for my projects, but you are welcome to follow them in your own projects as well :)
The rules here are loosely based on both the Linux kernel coding style as well as GNU Coding Standards, with tweaks based on my personal preferences.
Most of these rules can be applied to all programming languages, although the rules here are targeted at writing C++ code.
1. General
- Code must target the latest C++ standard and should compile without warnings on modern compilers with
-Walland-Werrorflags. - Non-standard language extensions, such as the GNU extensions, are allowed, however features implemented using the extensions should also be implemented using similar extensions from other compilers.
- All files in the project directory must use UTF-8 encoding, Unix line endings, and a trailing new line.
- A single line of code cannot exceed 100 characters in length (except when breaking the line makes the code harder to read).
- Use tabs for indentation (preferred width: 8), use spaces only for alignment.
- Main development target should always be Linux.
2. Naming
- Any identifier should use some variant of snake case: lowercase for functions, variables, structs, enums and filenames; uppercase for constants and macros. Global variables (if used) should use upper snake case.
- Names should be descriptive, purpose can be inferred from the name alone; avoid putting the return/variable type in the name.
-
Pointer and reference symbols should be placed next to the variable name, not the type.
i32 *ptr = NULL; i32 *ref = &ptr; -
Enums should always be defined as
enum classto prevent implicit conversions. If possible, also specify the underlying type:enum class profile_type : u8 { NORMAL = 0x01, ADMINISTRATOR = 0x02, GUEST = 0x03 };
3. Declaration and initialization of variables
- Declare one variable per line, this helps with both debugging and readability.
- Initialize as close to first use as possible.
- Prefer using
constorconstexprto#definewhen possible. - Avoid variables with global scope whenever possible.
- Keep the amout of local variables (e.g. in a function) below 10, unless strictly required.
- For integer variables prefer using fixed width integers, such as
int8_t,int16_t,int32_t,int64_tand their respective unsigned variants.
4. Functions
- A function should perform a single task.
- The length of a function is inversly proportional to the complexity of it. This means that a simple function which is essentially a long
switchblock (or manyifstatements) can be longer, however for non-trivial functions try to keep them under 60 lines in length (excl. braces and comments) - Return early from a function if an error was encoutered.
- Use modern prototypes with full parameter types.
-
The return type should be on the same line as the function name and its parameter list. This is a thing that the GNU Coding Standards will try to make you believe, so in short do not do this:
do this instead:i32 fancy_function(...) { ... }i32 fancy_function(...) { ... } -
If the parameter list is getting a little long, it can be broken into separate lines like so:
Make sure the parameters are aligned, use tabs first, then spaces to refine the alignment.i32 fancy_function(i32 some_int, std::vector<std::vector<std::string>> long_vector, f64 floating_point, std::vector<std::string> another_vector) - If a function is not expected to be used anywhere, and is a helper function, make it
static(you can even try toinlineit). - If there is something that can go wrong in a function, make it return an integer, or
std::expected<T, E>. -
Mark functions which perform an action which can go wrong with the
[[nodiscard]]attribute. If the function returns anenum class, mark theenum classinstead of the function.[[nodiscard]] u8 read_file(const std::string_view filename); /* Alternatively on the enum class: * useful whenever multiple functions return the same types of codes */ enum class [[nodiscard]] status : u8 { OK, IO_ERROR, TIMEOUT }; status read_file(const std::string_view filename); status write_file(const std::string_view filename);
5. Includes and headers
-
All
#includedirectives should appear at the very top of the source file in this order: standard headers, external library headers, internal project headers.
Example:
Note the empty lines between#include <print> #include <vector> #include <string> #include <curses.h> #include <sqlite3.h> #include "util/typedefs.hpp" #include "util/file.hpp"#includedirectives, this is mainly to differentiate what is standard, third-party and internal. - Headers should only expose classes, functions and data structures that might be required by other source files.
-
In most of my projects I have a separate header which aliases common integer and floating-point types, which looks something like this:
If you decide to commit to one of my repositories where the above aliases are in use, you should use them as well. This is for code style consistency.#pragma once #include <cstdint> using u8 = std::uint8_t; using u16 = std::uint16_t; using u32 = std::uint32_t; using u64 = std::uint64_t; using i8 = std::int8_t; using i16 = std::int16_t; using i32 = std::int32_t; using i64 = std::int64_t; using f32 = float; using f64 = double; -
Every header must have include guards. Use either of those:
#pragma once
However for modern C++#ifndef HEADER_H #define HEADER_H ... definitions go here #endif // HEADER_H#pragma onceis more than enough. - If possible (and when they are finally fully supported by major compilers), make use of modules instead of
#include.
6. Error handling
- Never ignore return values of functions, handle both success and error paths appropriately.
-
Log errors to
stderr:std::print(stderr, ...); /* C++23 and later */ - Exit with non-zero codes on critical errors, in places where exiting the program altogether is unexpected to the user, return a non-zero code and handle it appropriately.
7. Command-line interface
-
All programs should include the version flag (
--version) as well as the help flag (both short-hand long--help). If any of them is encountered, the normal execution of the program should be stopped.
Example of the--versionflag output using the BSD-3-Clause license:[program name] [version] Copyright (c) [year] [creator] License: BSD-3-Clause <https://opensource.org/license/bsd-3-clause> This is free software: you are free to use, modify, and redistribute it under the terms of the BSD-3-Clause License. There is NO WARRANTY, to the extent permitted by law.
Example of the-h/--helpflag output:Usage: [executable name] [OPTIONS] [INPUT]... Arguments: [INPUT]... Path(s) to the file or directory Options: -h, --help Print this help text --version Print version and copyright text Source code is available at: <[repository link]>
Options themselves are prepended with 2 spaces, and the description is aligned to a single column. The executable name should ideally be taken fromargv[0]. - Flags/parameters should be distinct.
- If possible, provide both short and long flags/parameters.
- The CLI should be easy to use and be fully documented.
8. Formatting
-
Opening and closing braces should be on their own lines, nothing else other than semi-colons and comments can be on the same line.
There are exceptions to this rule however: when the character directly before the opening brace is the equals sign, and in the case of defining a simple array, where putting each brace on its own line would be wasting space and make the code a little more unreadable.
Example:
In every other case it should look like this:std::vector<std::string> vec = { "Hello, world!", "Sample Text", "Lorem Ipsum", ... }; std::array<i32, 3> arr = {1, 2, 3};if (condition) { do_stuff(); } else { do_other_stuff(); } i32 fancy_function() { return 0; } -
It is acceptable to skip the braces when there is only one line in the
if,else,forstatement(s). Therefore this is okay:
However when only one branch has a single line, then use braces for both:if (condition) do_stuff(); else do_other_stuff(); for (i32 i = 0; i < 5; i++) fancy_stuff();if (condition) { do_a_thing(); do_other_thing(); } else { do_single_thing(); } - Function declarations should have a leading blank line.
- Avoid putting too many blank lines.
- Keep indentation consistent with the rest of the codebase.
- In reference to the previous point, avoid deep nesting. Anything above 5 levels and you're definitely doing something wrong.
9. Comments
-
Use
/* ... */for single-line and multi-line comments, as well as when closing a namespace (specifically when it's long, but do it every time just to be consistent).
Example:namespace file { ... } /* namespace file */ - Do not place comments that explain why a variable exists, or what a function does, it should be visible from the name and/or code.
- Document public functions (function marked with
staticdo not need to be documented) with their brief purpose (but keep it really brief), parameter list and a return value.
10. Build and tools
- Use the included
Makefileto build the project. - Preferred compiler is
clang, however since the flags are mostly compatible withgcc, you can use it as well (but remember to change it in theMakefile). - The compiler should not emit any warnings or errors.
- Treat the compiler as your friend.
11. Version control
- Commit messages should be descriptive, avoid vague
"do stuff"commit messages. - Avoid large monolithic commits.
12. Licensing
- Use a license which fits best for the project.
13. Large Language Model policy
- The use of LLMs is permitted for brainstorming, spell-checking and suggestions.
- LLMs should not be used to write the majority of the code.
-
Any code generated by an LLM which was directly (and sensibly) pasted into the codebase should be marked as such. This includes modifications to existing code.
I went ahead and generated an example function for the purposes of this point:
Note how there is a reason. This should show why an LLM was used in the particular case./* The function below has been generated by an LLM - 29/10/2026 Claude Sonnet 5.5 * Reason: needed a reliable way of getting smallest power of 2 larger than a value */ u32 next_pow2(u32 x) { if (x <= 1) return 1; return x > (UINT32_C(1) << 31) ? 0 : UINT32_C(1) << (32 - std::countl_zero(x - 1)); }
14. Additional notes
- Lines of code are not the best project quality metric. You can have a 5000 line monolith doing half of the things done by a well-written and optimized 500 line program.
- In general, you shouldn't touch code which works, however once in a while take a look at the codebase and see if there's some room for improvement in terms of readability and/or performance.