Skip to content

Instantly share code, notes, and snippets.

@X547
Last active June 18, 2026 12:35
Show Gist options
  • Select an option

  • Save X547/2445dac08268c0a010d858b762ee05ac to your computer and use it in GitHub Desktop.

Select an option

Save X547/2445dac08268c0a010d858b762ee05ac to your computer and use it in GitHub Desktop.

C++ Code Style Guide

This document defines the coding conventions for this codebase. Follow these rules in all new code and when modifying existing code. The rules are mandatory unless a specific situation makes them impossible, in which case prefer the spirit of the rule and keep the deviation local and obvious.

1. Formatting

1.1 Indentation

  • Indent with tabs only, one tab per nesting level. Never use spaces to produce indentation.
  • Use spaces only for table-like alignment after the first non-whitespace character on a line (for example, lining up the columns of a multi-line expression). Indentation is tabs; alignment is spaces.
  • The tab indent level of a line may be at most one greater than the indent level of the previous line. Do not skip levels (for example, jumping from one tab to three) to align with an unrelated construct.

1.2 Braces

  • Place the opening brace of a function or method definition on its own line:

    void WlResource::Destroy()
    {
        wl_resource_destroy(ToResource());
    }
  • Place the opening brace of a class, struct, enum, namespace, and control-flow statement (if, else, for, while, switch) on the same line:

    class HaikuCompositor: public WlCompositor {
    public:
        void HandleCreateSurface(uint32_t id) override;
    };
    
    if (resource == nullptr) {
        return nullptr;
    }
  • A trivial inline body of a function or method may stay on a single line with the braces:

    struct wl_resource *ToResource() const {return fResource;}
    void Unset() {fPtr = nullptr;}

1.3 Statement bodies always use braces

  • The body of every if, else, for, while, do, and switch must be enclosed in {}, even when it is a single statement. There are no brace-less control-flow bodies.

    if (!global.IsSet()) {
        return nullptr;
    }
    
    if (fPtr != nullptr) {
        fPtr->LockLooper();
    }

1.4 Spacing

  • One space after control-flow keywords: if (, for (, while (, switch (.
  • No space between a function name and its parenthesis: Init(, Create(.
  • Bind the pointer/reference marker to the variable name, not the type: struct wl_resource *fResource, HaikuSurface *surface, BPoint &pt.

1.5 Bracket and delimiter alignment

  • An opening delimiter ((, {, [) and its matching closing delimiter must sit at the same indent level when the construct spans multiple lines. The closing delimiter lines up under the start of the line that opened it, not under the content:

    fPendingState.viewportSrc = {
        x, y,
        width, height
    };
    
    window->SetSizeLimits(
        minWidth,
        maxWidth,
        minHeight,
        maxHeight
    );

1.6 Line wrapping

  • Do not mechanically wrap a long line at an arbitrary column. Wrapping must follow the structure of the code.

  • When an argument list, array initializer, or similar comma-separated construct does not fit on one line, switch to one item per line:

    HaikuSurface *Create(
        struct wl_client *client,
        uint32_t version,
        uint32_t id
    );
  • As an exception, semantically related items may share a line even in one-item-per-line layout — for example paired coordinates or dimensions:

    BRect rect = {
        left,  top,
        right, bottom
    };

1.7 Blank lines and section separators

  • Separate top-level definitions with two blank lines. Separate logical groups of members or statements within a body with a single blank line.

  • Leave two blank lines after the include section, before the first declaration or definition.

  • Two blank lines between code blocks may be used as a lightweight section separator.

  • Precede a //#pragma mark - section banner (see section 9) with two blank lines and follow it with one blank line:

    }
    
    
    //#pragma mark - HaikuSurface
    
    HaikuSurface::~HaikuSurface()
    {
        ...
    }

1.8 Whitespace and line endings

  • Use LF line endings.
  • Trim trailing whitespace from every line.
  • End every non-empty source file with exactly one trailing newline. An empty file stays empty.

2. Naming

2.1 Types

  • Name classes, structs, and enums in PascalCase: HaikuSurface, FrameCallback, ViewportSrc.
  • Prefix a class that wraps a protocol/global object with its domain prefix consistently (for example Haiku* for platform-backed implementations, Wl* for protocol base wrappers). Keep the prefix uniform across the layer.

2.2 Functions and methods

  • Name functions and methods in PascalCase: Dispatch, FromResource, AttachWindow.
  • Name a method that handles an incoming protocol request Handle<Request>: HandleCommit, HandleCreateSurface, HandleSetOpaqueRegion.
  • Name a method that emits an outgoing event Send<Event>: SendDone, SendConfigure, SendRelease.
  • Name a static downcast/lookup helper FromResource (or From<Source>), and a static factory Create.

2.3 Variables

  • Prefix non-public data members with f: fResource, fState, fSurface.

  • Prefix mutable global variables with g: gServerHandler, gServerMessenger.

  • Prefix file-local static variables with s:

    static int32_t sStaticGlobalVar = 123;
  • Name local variables in camelCase: viewLocked, pendingState, envValue.

  • Name enumerators of an unscoped enum in PascalCase with a shared semantic prefix that groups them, rather than ALL_CAPS:

    enum StateField {
        FieldBuffer,
        FieldOffset,
        FieldTransform,
    };

    In a scoped enum class (preferred — see section 10) the enum name already scopes the members, so the prefix is unnecessary: Mode::None, Mode::Server.

2.4 Constants and macros

  • Name constants — both global and static/const/constexpr class data members — in camelCase with a k prefix:

    constexpr int32_t kSomeConst = 123;
  • Reserve ALL_CAPS for macros (including include guards). Prefer a typed constexpr constant over a #define whenever a macro is not actually required.

3. Files and Headers

  • Start every project header with #pragma once. Use traditional #ifndef/#define include guards only when a file must remain portable to environments without #pragma once support.
  • Order includes in the following groups, separated by a single blank line:
    1. The corresponding header for this source file (in a .cpp).
    2. Standard C library headers (<stdio.h>, <stdlib.h>, ...).
    3. Standard C++ library headers (<optional>, <utility>, ...).
    4. Global <> headers (framework and third-party).
    5. Local "" headers (project headers).
  • Use forward declarations in headers instead of #include whenever only a pointer or reference to a type is needed. Include the full definition only in the .cpp (or where the layout is required).
  • Keep one primary class per header/source pair, named after the class. Small private helper classes local to one translation unit may live inside the .cpp.

3.1 Namespaces

  • When a module's declarations are wrapped in a namespace, the opening brace goes on the same line as the namespace keyword (section 1.2); close it with a trailing comment naming the namespace. Leave two blank lines after the opening brace and two blank lines before the closing brace, matching the file-level spacing of section 1.7:

    #include <stdio.h>
    
    
    namespace SomeNamespace {
    
    
    void Do1();
    void Do2();
    
    
    } // SomeNamespace
  • Do not introduce names at global (file) scope with a using directive (using namespace Foo;) or a using declaration (using Foo::Bar;), in either a header or a source file — it pollutes every translation unit that sees the name. Confine such using statements to the narrowest scope that needs them (a function body, or a namespace block). This restriction does not apply to using type aliases (section 10), which declare a new name rather than import an existing one.

4. Classes and Object Lifetime

4.1 Construction

  • Do fallible initialization in the constructor and report failure with an exception (see section 8). A fully constructed object is then always valid, so callers need no separate "is it ready?" check.

  • Format the member initializer list with the colon directly after the closing parenthesis of the signature, then one initializer per line indented one level, and the opening brace on its own line (per section 1.2). List the base class(es) first, then members in declaration order — the order they are actually initialized:

    Object::Object(int32_t a, int32_t b, int32_t c):
        Base(a),
        fB(b),
        fC(c)
    {
        // ...
    }

4.2 Member declaration

  • Initialize members at the point of declaration. Use {} for value-initialization and = nullptr for pointers that need an explicit null:

    struct wl_resource *fResource = nullptr;
    uint32 fPendingFields {};
    WaylandView *fView {};
  • Declare private members first, then public, unless readability strongly favors otherwise. Group related members together.

  • Grant access to collaborating classes with explicit friend declarations rather than widening visibility or adding pass-through accessors.

4.3 Virtual interfaces

  • Declare base-class polymorphic methods virtual and give every base class a virtual destructor (virtual ~T() = default; when trivial).
  • Mark an override that is meant to be further overridden with override, and one that must not be overridden again with final. Always use one or the other on an overriding method.
  • Express an abstract contract with pure virtual methods (= 0).

4.4 Accessors

  • Write getters as short inline const methods named after the property, returning the stored value directly:

    uint32_t Id() const {return wl_resource_get_id(fResource);}
    HaikuXdgSurface *XdgSurface() {return fXdgSurface;}

4.5 Const correctness

  • Const correctness is expected, not optional. Apply const everywhere it holds:

    • Mark a method const when it does not modify the observable state of its object — every accessor in section 4.4, and any other query method.
    • Take a parameter by const reference or const pointer when the callee only reads it (the borrow case in section 6.1).
    • Qualify a local or member with const when it is initialized once and never reassigned.

5. Memory and Resource Management

  • Use RAII wrappers for every resource that must be released: scope-bound smart/owner types (ObjectDeleter, AreaDeleter, FileDescriptorCloser) for ownership, and reference-counted base types (BReferenceable / BReference) for shared ownership.

  • Use scope-guard objects for paired acquire/release operations (locks, contexts) so the release happens automatically on every exit path:

    auto viewLocked = AppKitPtrs::LockedPtr(this);
    // unlocked automatically at end of scope
  • Transfer ownership out of an owner wrapper explicitly with Detach(); query it with IsSet(). Do not bypass the wrapper to access the raw resource while it owns it.

  • Tie an object's lifetime to its protocol resource where that is the model: handle destruction through the resource's destructor callback, and let last-reference release trigger teardown for reference-counted objects.

6. Pointers, References, and Null

  • Use nullptr for null pointers — never NULL or 0.
  • Compare pointers explicitly against nullptr: write if (ptr != nullptr) and if (ptr == nullptr), not if (ptr) or if (!ptr). (A wrapper object that defines operator bool/IsSet() is tested with that member instead.)
  • Guard every pointer dereference that can legitimately be null, and return early on the null case.

6.1 Reference vs pointer parameters

The choice between a reference and a pointer parameter carries meaning about lifetime and ownership; pick the one that states the intent.

  • Pass by reference (T&, const T&) to express borrow semantics: the callee may read or modify the object only for the duration of the call. It must not store the address or use it after returning. Prefer a reference wherever only borrowing is intended.
  • Pass by pointer when the address legitimately outlives the call — the callee may register the pointer in its own internal state, or the call may transfer ownership of the pointee. A pointer parameter is therefore a signal that "this address may be kept or handed over", which a reference would not imply.
  • For ownership transfer, prefer moving an RAII owner wrapper (section 5) over a raw pointer, so the hand-off is explicit and leak-safe.

7. Control Flow

  • Prefer early returns (guard clauses) over deep nesting. Validate inputs and bail out at the top of the function:

    void HaikuSurface::Detach()
    {
        if (fView == nullptr) {
            return;
        }
        // ... main body, not nested ...
    }

7.1 switch statements

  • Use switch for dispatch over an enum or opcode. Keep case bodies short, delegating to helpers when they grow.

  • Indent case labels one level deeper than the switch keyword.

  • End every branch with break, including the default branch.

  • When a case declares its own local variables, wrap its body in {}, with the opening brace on the same line as the case label:

    switch (field) {
        case FieldOffset: {
            int32_t dx = NextX();
            ApplyOffset(dx);
            break;
        }
        case FieldScale:
            ApplyScale();
            break;
        default:
            break;
    }
  • Several case labels stacked with no code between them (only the last carries the body) read as "any of these options", not as fall-through, and need no comment:

    switch (colorSpace) {
        case B_RGBA32:
        case B_RGBA64:
        case B_RGBA15:
            mode = B_OP_ALPHA;
            break;
        default:
            mode = B_OP_COPY;
            break;
    }
  • Genuine fall-through — a case with code that then continues into the next — is rare. Where it is truly needed, add a comment explaining why. Likewise, sharing locals across cases (the unusual reason to omit per-case braces) should be explained.

8. Error Handling and Diagnostics

8.1 Prefer exceptions

  • Prefer exceptions over error codes for reporting failure. A function that cannot complete throws; callers that cannot proceed let the exception propagate. This keeps the success path free of error-checking noise.

8.2 Wrapping error codes from external libraries

  • When an external (typically C) library returns an error code, wrap it in a std::system_error at the boundary, so the rest of the code deals only in exceptions. Use a small helper to convert-and-throw:

    void CheckThrow(status_t res)
    {
        if (res < B_OK) {
            throw std::system_error(res, std::generic_category());
        }
    }
    
    void Do()
    {
        CheckThrow(LibCall1());
        CheckThrow(LibCall2());
    }

8.3 Working with error codes locally

  • Where it is genuinely necessary to keep handling raw error codes (a tight boundary, performance-critical path, or code that must stay code-based), use a check-and-early-return macro rather than open-coding the test at every call:

    #define CHECK_RET(err) {status_t _err = (err); if (_err < B_OK) return _err;}
    
    status_t Do()
    {
        CHECK_RET(LibCall1());
        CHECK_RET(LibCall2());
        return B_OK;
    }

8.4 Providing error codes back to an external library

  • When a callback invoked by an external library must return an error code (and must not let an exception escape across the C boundary), catch everything at the boundary and convert the exception back into a code:

    status_t Callback()
    {
        try {
            DoWork();
            return B_OK;
        } catch (const std::system_error &e) {
            return e.code().value();
        } catch (...) {
            return B_ERROR;
        }
    }

8.5 Diagnostics

  • Use an assertion helper for invariants that must hold (Assert(cond) that calls abort()), and reserve the debugger trap for programming errors that indicate a corrupt state.
  • Report unexpected-but-recoverable conditions to stderr with a recognizable prefix when an exception is not appropriate (for example deep inside a callback).
  • Remove dead debug output before committing.

9. Comments

  • Mark major sections within a translation unit with a banner comment so the file reads as a sequence of clearly delimited components:

    //#pragma mark - HaikuSurface
  • Prefix actionable follow-ups with TODO: and state what needs to happen.

  • Write comments that explain why, not what the code obviously does. Keep them current with the code they describe.

  • Write code as if it were written from scratch in its final form. Do not leave comments that describe what changed, what the code used to be, or why an edit was made (// was 5, now 10, // removed old loop, // fixed bug). That history belongs in Git commit messages, not in the source.

10. Language Features

  • Target modern C++ and use the standard library where it fits: std::optional for maybe-present values, std::move to transfer ownership, std::min/std::max, and <bit> utilities for bit manipulation.

  • Prefer enum class (scoped, strongly typed) over a plain enum. Use an unscoped enum only where implicit conversion to the underlying integer is genuinely required (for example a bit-flag field built with |).

  • Use auto when the initializer makes the type obvious (especially for verbose wrapper types), and spell the type out when it aids comprehension.

  • Use a C++ named cast (static_cast, reinterpret_cast, const_cast) rather than a C-style cast, so the kind and intent of the conversion is explicit and greppable:

    auto *surface = static_cast<HaikuSurface*>(wl_resource_get_user_data(resource));
    auto *pixels = reinterpret_cast<uint8_t*>(data);
  • Declare type aliases with using, not typedef — it reads left-to-right and works with templates:

    using SurfaceList = DoublyLinkedList<HaikuSubsurface, SubsurfaceLink>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment