Vai al contenuto

C++ Conventions

Questo contenuto non è ancora disponibile nella tua lingua.

This document defines the standards and conventions you must follow when writing C++ code in Saturn. These conventions exist to ensure long-term maintainability, readability, and predictable behavior across the entire engine. You should follow these rules for all engine code, tooling, and internal libraries.

These conventions are derived from real development constraints encountered while building a game engine. The rules are inspired by the Google C++ Style Guide but adapted to Saturn’s architecture, performance requirements, and contributor workflow. Every guideline addresses either a known failure mode or a scalability concern.


Game engines are large, tightly coupled systems operating under strict performance and correctness constraints. You will routinely work across rendering, asset loading, ECS, platform abstraction, and tooling layers. In this environment, inconsistency introduces cognitive overhead and increases the likelihood of subtle bugs.

Saturn conventions prioritize clarity over expressiveness. You should write code that communicates intent directly and unambiguously. Explicit naming, predictable structure, and visible lifetimes matter more than clever language features. When debugging complex behavior under pressure, readable code reduces error recovery time.

Consistency also enables collaboration. Contributors should be able to navigate unfamiliar code without reconstructing personal conventions. These rules establish a shared mental model for how Saturn code is written and organized.


Use automated tooling to enforce formatting rules. Manual formatting decisions are discouraged.

Saturn standardizes on Clang Format and Clang Tidy. Formatting and static analysis rules are defined in the project configuration files and apply uniformly across the codebase. You must not introduce formatting deviations.

Install a Clang Format integration for your IDE and enable format-on-save. This configuration ensures that all submitted code conforms automatically and avoids formatting-related review noise.


Saturn uses CMake as its build system. You should follow existing CMake patterns and avoid introducing custom abstractions unless strictly necessary. Despite its limitations, CMake provides broad platform support and integrates cleanly with Saturn tooling.

Saturn uses VCPKG as the primary dependency manager. When a dependency is unavailable or unsuitable in VCPKG, you must add it as a Git submodule under the vendor directory. This approach balances convenience with explicit version control and reproducibility.

You should not introduce alternative package managers or ad-hoc dependency fetching mechanisms.


Testing in Saturn focuses on correctness where deterministic validation is possible. You should treat tests as a safety net for logic that can fail silently or propagate errors across systems.

Saturn uses Google Test for both unit and integration tests. You should integrate all tests into the standard build pipeline.

Write unit tests for algorithmic and logic-heavy components. This category includes math utilities, serialization code, data structures, and ECS primitives. These systems have clear inputs and expected outputs and benefit most from strict validation.

Unit tests should be deterministic and isolated. Avoid dependencies on platform state, timing, or external resources.

Use integration tests to validate interactions between systems. Examples include asset pipeline validation, ECS lifecycle behavior, and renderer initialization paths. These tests detect failures caused by incorrect assumptions between subsystems.

Integration tests should verify behavior, not implementation details.

Do not test rendering output at the pixel level. Driver variability and hardware differences make such tests unreliable. Do not test third-party library internals. Instead, test Saturn’s integration points and error handling.

Avoid tests for non-deterministic systems unless determinism can be enforced through seeding or mocking.


Organize class declarations to make large headers navigable and intention-revealing. When a class grows beyond trivial size, group related members explicitly.

Use comment-based section headers to separate constructors, public methods, private helpers, and state. This structure improves scanability and reduces navigation time in large files.

class ExampleClass {
public:
// Constructors and destructors
ExampleClass();
~ExampleClass();
// Public methods
void initialize();
void update();
private:
// Member variables
int m_counter;
float m_speed;
};

You may optionally annotate high-level design patterns used by a class, such as // Singleton or // Factory. Use these annotations sparingly and only when they add clarity during refactoring or review.


Naming rules exist to encode scope, lifetime, and intent directly into identifiers. You should treat naming as part of the API contract.

Use camelCase for functions. Prefix function parameters with an underscore to distinguish them from local variables and member fields.

void processData(int _inputData) {
int processedData = _inputData * 2;
}

This convention reduces shadowing errors and makes data flow visible during review.

Use PascalCase for classes, structs, and type aliases. This convention visually distinguishes types from functions and variables.

Avoid using namespace directives in headers. You may use them in implementation files when scope is limited and unambiguous.

Prefix variables to encode ownership and lifetime:

  • Instance fields use m_
  • Static fields use s_
  • Global variables use g_
class MyClass {
private:
int m_id;
static int s_instanceCount;
};
int g_applicationState;

This convention makes lifetime explicit at the point of use and reduces ambiguity in complex systems.

Prefix compile-time constants with k_.

const int k_defaultTimeout = 30;
const float k_maxSpeed = 100.0f;

Use macros only when no language alternative exists. Restrict macros to source files whenever possible. Write macro names in ALL_CAPS to make them visually distinct.

Use enum class instead of unscoped enums. Scoped enums prevent name collisions and improve readability.

Use snake_case for enum values.

enum class RenderMode {
wireframe,
shaded,
textured,
};

Name macro parameters using the same rules as function parameters, but start each parameter with an uppercase letter. This convention distinguishes generated identifiers from fixed macro text.

DEFINE_CLASS_WITH_MEMBER(_Type, _Member, _Name) \
class _Name { \
public: \
_Type get##_Member() const { return m_##_Member; } \
private: \
_Type m_##_Member; \
};

Documentation exists to explain intent, constraints, and safe usage. You should write documentation for future contributors who lack your current context.

Saturn uses Doxygen for API documentation generation. You must write Doxygen-style comments for all non-trivial public and internal APIs.

Document functions, classes, and systems that have side effects, performance implications, or complex behavior. Focus on why the code exists and how it must be used safely.

Always document:

  • Preconditions and postconditions
  • Ownership and lifetime rules
  • Thread-safety guarantees
  • Units of measurement
/**
* Initializes the graphics subsystem and prepares rendering backends.
*
* Must be called before any rendering operations. This function allocates
* GPU memory and initializes platform-specific pipelines.
*
* @return True if initialization succeeds.
*/
bool Renderer::initialize();

Avoid documenting trivial accessors or self-explanatory functions.

Place API documentation in headers. Use file-level Doxygen comments for subsystems or modules that require contextual explanation.

Write higher-level architectural documentation in standalone Markdown files under the docs/ directory.

Use comments in implementation files to explain non-obvious control flow, performance-sensitive code, or subtle algorithmic behavior. Do not restate what the code already expresses clearly.


Mirror namespace structure in the directory layout. This alignment reduces mental overhead and makes symbol ownership obvious.

Reserve internal and detail namespaces for implementation details that external users must not depend on.

Prefer explicit naming such as saturn::platform::win32::Win32Platform. Verbosity is acceptable when it prevents ambiguity and reduces reliance on using directives.

As systems grow, migrate them into dedicated subdirectories rather than flattening namespaces.


Avoid exceptions for recoverable errors. Exceptions introduce unpredictable control flow and platform-specific behavior that complicates engine code.

Use Saturn’s Result type for recoverable failures that require error propagation.

Result<Texture, std::string> loadTexture(const std::string& path) {
if (path.empty()) {
return Err<Texture, std::string>("Invalid texture path");
}
return Ok<Texture, std::string>(std::move(texture));
}

Use bool for simple success or failure cases. Use std::optional when absence is meaningful but error context is unnecessary.

Reserve exceptions for unrecoverable errors where termination is the correct response.


Separate construction from initialization to make failure handling explicit.

Constructors must establish invariants and perform trivial setup only. Constructors must not fail.

Perform fallible work in explicit initialization methods that return Result or equivalent. Use factory functions to combine construction and initialization when appropriate.

This pattern guarantees that objects are either fully valid or do not exist.


Apply abstraction proportionally. Inline platform-specific logic is acceptable for small, localized differences. Use preprocessor directives and comments when this improves clarity.

Introduce abstraction layers only when platform divergence is substantial or growing. Avoid unnecessary indirection that obscures control flow.