FOnline Engine
Current 2026.1.100-dev GitHub
Documentation Docs/en/contributing/coding-contracts/class-layout.md

Class Layout and Result Use

This contract applies to authored native Engine code and project-native extensions. Third-party code retains its upstream conventions.

Passive records and classes

Use struct only for passive records of public data. Do not declare constructors, destructors, methods or member operators, including explicitly defaulted special members and comparisons. They do not inherit behavior, enforce invariants or use final. Initialize record fields with braces, using {} for the intended default state; see braced initialization. Place functions that create, inspect, compare or modify records outside the record as free functions; use nonmember operators where appropriate.

The passive-record restriction does not apply to Source/Essentials/. Structs there may have constructors, methods and member operators, including in foundational value types, internal data records and template/interface adapters. Preserve their natural API; do not replace it with free functions or change the keyword solely to satisfy the passive-record rule.

Settings types (including generated groups and their base/aggregate settings) and all types marked ExportValueType are also exempt, in every native layer and project extension. Retain their struct constructors, methods, member operators and natural data API. For example, mpos, msize and TextPackKey keep their member interfaces, and native settings retain settings.Group.Name. An exception does not require converting an existing class to a struct.

When a type needs controlled state or a custom lifetime protocol, use class. Encapsulate its representation, expose only the required public interface and keep implementation details private. A keyword change followed by making all fields public does not establish encapsulation. Choose the type from its responsibilities; do not add getters merely to preserve a record’s old method syntax. Classes follow the method order and result-use rules below.

Comment spacing

A comment never requires a preceding blank line. This applies everywhere, including function and lambda bodies, enums and struct/class member lists. Choose blank lines from the semantic grouping of code and declarations. Keep comments immediately above the code they describe. The rules for separating semantic stages still apply.

Class method order

Place construction, assignment and destruction first. Follow them with an upper public block containing queries about the object’s state or represented data. Then place operations, ordered from initialization to teardown. Preserve access levels and keep related method families together within the appropriate block. Preserve the existing relative order within each block; move only the methods needed to establish these groups. Nested types and aliases must remain available to declarations that use them.

An information method answers a question about the object: its properties, contents, available entries, current status or related objects. It may return a field, value, owned string/container, borrow, reference, span or iterator. Queries can search, compare data, derive a value, read storage, lock or populate an internal cache. These implementation details do not disqualify a query. Preconditions and returning a mutable borrow or reference do not disqualify it either. A class without such queries needs no information block.

Mutations, consuming reads/cursor advancement, resource creation, waiting for work and substantial processing as the method’s main purpose are operations. They belong below the information block even when they are const or return a value. Judge the logical contract and observable side effects, rather than treating every lookup, allocation or I/O call as a separate operation. Read implementations and callees when the purpose is unclear. Classify virtual declarations by their contract; a trivial stub does not turn an operation into an information method.

For example, CacheStorage::HasEntry(), GetString(), GetData() and GetDataBounded() request information, even when they search or read a file and return allocated data. ConfigFile::HasSection() and FileHeader::GetNameNoExt() also belong in the information block. CacheStorage::SetDataChecked() changes stored data, FileHeader::Copy() explicitly creates a copy, and FileReader::ReadUInt8() advances the reader: these remain operations.

[[nodiscard]]

In Source/Essentials/, apply [[nodiscard]] wherever ignoring a result violates the function’s purpose or loses a required value, resource or failure status. This includes suitable free functions, factories, conversion/arithmetic helpers and I/O results. Fluent mutators and assignment/stream operators whose result is optional chaining do not need the attribute merely because they return a value.

Outside Essentials, use [[nodiscard]] only on public instance methods in the upper information block, and annotate all methods in that block. Preserve existing query annotations by default; remove one only for a concrete semantic reason. Remove it from operations, free functions, static methods, nonpublic helpers, out-of-class definitions and types. Do not move an operation into that block to justify an attribute. A Get/Is name, return type, const or noexcept is not a classifier. Mixed macro-generated method families must not acquire a blanket attribute when some expansions mutate or perform operations.

When intentionally discarding an annotated result, use an explicit (void) cast at the call site after checking that discarding it is valid. Do not suppress a warning that exposes an unhandled failure or lost ownership.

Review declaration moves for template constraints, comments, conditional branches and access levels. Preserve implementations, defaults and field order. Rebuild native consumers together when virtual declarations move; the attribute and declaration grouping do not change serialization or network formats.

Related contracts: local variables, exception safety and Essentials.

Start typing to search.