Jump to content

Coding Style (OpenApoc)

From UFOpaedia
Revision as of 23:05, 1 March 2026 by Deldonut1 (talk | contribs) (Re-add 9 coding conventions from CODE_STYLE.md that were missing: comments style, trivial one-line functions, ownership guidance, static_assert, iterator exception, forward declaration limits, Yoda conditionals, numeric_limits, short variable names)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This document specifies the guidelines for writing and formatting the C++ code that forms the core of OpenApoc.

OpenApoc uses C++17. This requires reasonably modern compilers (GCC 8+, MSVC 2019+, Clang 7+ have been tested). You should avoid using compiler-specific extensions where possible. Exceptions exist, but should be wrapped in an #ifdef.

Code formatting is enforced by clang-format (version 18). The configuration file .clang-format in the root of the OpenApoc source repository defines the project style.

Static analysis is enforced by clang-tidy (version 18). The CI pipeline rejects unformatted code and code that fails tidy checks.

It is highly recommended to run clang-format on all modified files before committing:

clang-format -i path/to/file.cpp path/to/file.h

When run from the root of the OpenApoc source repository, it automatically uses the supplied .clang-format configuration file.

When using the CMake build system, there are dedicated targets:

# Format all source files (from build directory)
cmake --build build -t format-sources

# Run static analysis (from build directory)
cmake --build build -t tidy

Indent

  • Tabs for indenting, spaces for alignment, indenting by 1 tab for each new scope
void function()
{
	reallyLongFunctionNameWithLotsOfArguments(argOne, argTwo,
	                                          argThree);
}
  • Avoid going over 100 columns (at tab width of 4 spaces)
    • If you find yourself going over this, it's often a hint to try to pull things out of loops / into functions
    • Don't break strings up to fit this; it looks ugly and makes things harder to read
  • If you have to break, indent the following line by an extra tab
    • When breaking a single statement, break the line before the next operator. Avoid having an operator as the last thing on a line.
void reallyLongFunctionNameIMeanThisIsReallyBadlyNamed(int parameterOne,
	int paramTwo, char theThirdOne)
{
	if (parameterOne == yetAnotherReallyLongCondition
		&& youHaveBetterThingsToDo)
	{
		doWhatever();
	}
}

Whitespace

  • Spaces before and after operators
	a = b;
	a && b;
	a + b;
  • Space after if/while/else/for, space after :/; in for
	for (auto &a : b)
  • No spaces after function name (or function-like keywords like sizeof), but space after flow control keywords, space after comma for multiple arguments
	func(a, b);
	if (a == 0)
  • References and pointers: & and * align right (to the variable), not to the type
	float *pointerToFloat;
	Type &ref;

Scope

  • Indent 1 tab for each new scope
  • New scope is always surrounded by {} braces, even for single-statement blocks
  • Opening brace { goes on the next line at the indent of the enclosing scope (Allman style)
  • Closing brace } at the same indent as the opening {
  • New scopes are caused by:
    • Functions
    • Conditional blocks (if/else/while/for)
    • switch/case
void functionDefinition()
{
	newScopeHere();
}
	if (x)
	{
		doWhatever();
	}
	else if (y)
	{
		doWhateverTheSecond();
	}
	else
	{
		doThatLastThing();
	}
  • switch always has a default case unless switching over an enum class where every value is handled
  • All case sections should have a break
  • case braces {} are optional, based on whether new stack variables are needed
	switch (a)
	{
		case A:
			doSomething();
			break;
		case B:
		{
			auto newVariable = getSomething();
			useIt(newVariable);
			break;
		}
		default:
			break;
	}
  • Exception: Trivial functions (a single statement fitting within 100 columns) may use the definition and body on one line:
int getX() const { return x; }

This is enforced by clang-format (AllowShortFunctionsOnASingleLine: All).

Naming

Style Used For
CamelCase Classes, enums, enum class members, namespaces, template parameters
camelBack Methods, member variables, function parameters, local variables
SHOUTY_CAPS Constants, macros
lower_case Labels

Short variable names (i, x, y) are fine when the meaning is obvious from context.

Types

  • Use auto liberally, especially when the type is obvious from the right-hand side. Use auto & to avoid copies.
  • Use enum class over plain enum
  • Use struct only for data-only types; structs must never use access modifiers
  • No C-style casts — use static_cast<>, dynamic_cast<>, reinterpret_cast<>
  • Prefer {} brace initialization
  • Prefer std::numeric_limits<> over C-style MAX_INT / INT_MAX macros

Smart Pointers

OpenApoc provides short aliases in library/sp.h:

Alias Equivalent
sp<T> std::shared_ptr<T>
up<T> std::unique_ptr<T>
wp<T> std::weak_ptr<T>
mksp<T>(args...) std::make_shared<T>(args...)
mkup<T>(args...) std::make_unique<T>(args...)
  • No naked new — always wrap in smart pointers immediately
  • Prefer up<> (exclusive ownership) over sp<> unless shared ownership is genuinely needed
  • Use std::move() to transfer up<> ownership
  • Ownership guideline: If ownership of a member is tied to the class, use a direct member (no pointer). If a reference owned by another object is guaranteed to outlive the class, use a &reference member. Otherwise use up<> or sp<> depending on whether ownership is exclusive or shared.

Templates

  • Template type parameters should use CamelCase
  • Place template<...> on the line above the function/class declaration
template <typename ValueType>
ValueType doSomething(ValueType input)
{
	return input;
}

Class Declarations

  • public:/private:/protected: written at class indent level (not indented further)
  • Always explicitly write private:, even though it is the default
  • virtual only on the base class; override on derived classes — never both together
  • All classes with virtual methods must have a virtual destructor
  • Use = default instead of empty {} constructor/destructor bodies
  • Use member initializer lists; initializer order must match declaration order
class MyClass : public BaseClass
{
private:
	int memberVariable = 0;
	UString name;
public:
	MyClass() = default;
	~MyClass() override = default;

	void doSomething() override;
	int getValue() const;
};

Functions

  • Functions should be named camelBack
  • Use early return — prefer separate if (cond) return; checks over deeply nested conditionals
  • const aggressively: const member functions, const parameters, const return types, const local variables
  • Range-for loops: for (auto &element : container) / for (const auto &element : container)
    • Exception: When removing elements during iteration, use an iterator-based loop instead of range-for
  • Prefer emplace() over insert() in STL containers

General Code

  • Use anonymous namespaces instead of static for file-local functions and classes
namespace
{
void helperFunction()
{
	// file-local helper
}
} // anonymous namespace
  • All project code lives in namespace OpenApoc {}. Namespace content is not indented. Closing brace gets a comment:
namespace OpenApoc
{

class MyClass
{
	// ...
};

} // namespace OpenApoc
  • Use static_assert() to verify assumptions about alignment, packing (e.g., when reading structs from original game files), or template restrictions
  • Avoid Yoda conditionals — write var == 1, not 1 == var

Comments

  • Prefer // for single-line comments
  • Multi-line /* */ comments: align the * on each intermediate line
  • Use //TODO: and //FIXME: markers to flag work-in-progress or known issues
  • Do not comment for the sake of it — if the code can be made clearer, prefer that over adding a comment

Logging

Uses fmt-style format strings with positional {0}, {1} placeholders (not printf-style):

#include "framework/logger.h"

LogInfo("Loaded mod \"{0}\"", modName);
LogWarning("Value {0} exceeds limit {1}", value, limit);
LogError("Failed to load file \"{0}\"", path);
  • LogInfo — general information
  • LogWarning — recoverable errors
  • LogError — fatal errors

Strings

Use UString (from library/strings.h) for all strings. All char*/std::string values are assumed UTF-8.

String formatting uses the fmt library:

#include "library/strings_format.h"

UString result = OpenApoc::format("Player has {0} credits and {1} agents", credits, agentCount);

Translation:

UString translated = tr("English text to translate");

Includes and Headers

  • Use #pragma once (no traditional include guards)
  • Include order: Local headers first, then system headers. Each block alphabetically sorted.
  • Local headers use paths relative to the OpenApoc root: "framework/logger.h", not "../logger.h" or "logger.h"
  • Prefer forward declarations over #include in headers where possible
    • Forward declarations are not possible for templates, non-class types (e.g., enum class), and superclasses. When in doubt, try building without the include and see what fails.
#pragma once

#include "library/sp.h"
#include "library/strings.h"

#include <vector>

namespace OpenApoc
{

class ForwardDeclaredType;

class MyClass
{
private:
	int member = 0;
public:
	void publicFunction();
};

} // namespace OpenApoc

Key Review Expectations

These patterns are consistently enforced during code review:

  1. const correctness — if something can be const, it must be const
  2. Readable conditionals — avoid embedded comments in complex conditionals; prefer early-exit checks
  3. One logical change per PR — keep changes focused for clean history and bisectability
  4. Use auto — when the type is already visible on the RHS
  5. Prefer exclusive ownership — use up<> over sp<> when shared ownership is not required

See Also