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.
Conceptual explanation
Section titled “Conceptual explanation”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.
Code formatting
Section titled “Code formatting”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.
Build system and dependencies
Section titled “Build system and dependencies”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 strategy
Section titled “Testing strategy”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.
Unit testing
Section titled “Unit testing”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.
Integration testing
Section titled “Integration testing”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.
Non-goals
Section titled “Non-goals”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.
Class structure
Section titled “Class structure”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 conventions
Section titled “Naming conventions”Naming rules exist to encode scope, lifetime, and intent directly into identifiers. You should treat naming as part of the API contract.
Functions and parameters
Section titled “Functions and parameters”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.
Classes and types
Section titled “Classes and types”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.
Variables and scope prefixes
Section titled “Variables and scope prefixes”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.
Constants and macros
Section titled “Constants and macros”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,};Macro parameters
Section titled “Macro parameters”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 standards
Section titled “Documentation standards”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.
What to document
Section titled “What to document”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.
Where to document
Section titled “Where to document”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.
Implementation comments
Section titled “Implementation comments”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.
Architecture and layout
Section titled “Architecture and layout”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.
Error handling
Section titled “Error handling”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.
Object lifecycle
Section titled “Object lifecycle”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.
Platform abstraction
Section titled “Platform abstraction”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.