Jump to content

Coding Style (OpenApoc): Difference between revisions

From UFOpaedia
Deldonut1 (talk | contribs)
Major rewrite: updated to C++17/clang-format-18; added smart pointers; logging; strings; review expectations
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
 
Line 137: Line 137:
}
}
</pre>
</pre>
* '''Exception''': Trivial functions (a single statement fitting within 100 columns) may use the definition and body on one line:
<pre>
int getX() const { return x; }
</pre>
This is enforced by <code>clang-format</code> (<code>AllowShortFunctionsOnASingleLine: All</code>).


== Naming ==
== Naming ==
Line 151: Line 159:
| <code>lower_case</code> || Labels
| <code>lower_case</code> || Labels
|}
|}
Short variable names (<code>i</code>, <code>x</code>, <code>y</code>) are fine when the meaning is obvious from context.


== Types ==
== Types ==
Line 159: Line 169:
* No C-style casts &mdash; use <code>static_cast&lt;&gt;</code>, <code>dynamic_cast&lt;&gt;</code>, <code>reinterpret_cast&lt;&gt;</code>
* No C-style casts &mdash; use <code>static_cast&lt;&gt;</code>, <code>dynamic_cast&lt;&gt;</code>, <code>reinterpret_cast&lt;&gt;</code>
* Prefer <code>{}</code> brace initialization
* Prefer <code>{}</code> brace initialization
* Prefer <code>std::numeric_limits&lt;&gt;</code> over C-style <code>MAX_INT</code> / <code>INT_MAX</code> macros


== Smart Pointers ==
== Smart Pointers ==
Line 181: Line 192:
* Prefer <code>up&lt;&gt;</code> (exclusive ownership) over <code>sp&lt;&gt;</code> unless shared ownership is genuinely needed
* Prefer <code>up&lt;&gt;</code> (exclusive ownership) over <code>sp&lt;&gt;</code> unless shared ownership is genuinely needed
* Use <code>std::move()</code> to transfer <code>up&lt;&gt;</code> ownership
* Use <code>std::move()</code> to transfer <code>up&lt;&gt;</code> 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 <code>&amp;reference</code> member. Otherwise use <code>up&lt;&gt;</code> or <code>sp&lt;&gt;</code> depending on whether ownership is exclusive or shared.


== Templates ==
== Templates ==
Line 225: Line 237:
* '''<code>const</code> aggressively''': const member functions, const parameters, const return types, const local variables
* '''<code>const</code> aggressively''': const member functions, const parameters, const return types, const local variables
* Range-for loops: <code>for (auto &element : container)</code> / <code>for (const auto &element : container)</code>
* Range-for loops: <code>for (auto &element : container)</code> / <code>for (const auto &element : container)</code>
** '''Exception''': When removing elements during iteration, use an iterator-based loop instead of range-for
* Prefer <code>emplace()</code> over <code>insert()</code> in STL containers
* Prefer <code>emplace()</code> over <code>insert()</code> in STL containers


Line 254: Line 267:
} // namespace OpenApoc
} // namespace OpenApoc
</pre>
</pre>
* Use <code>static_assert()</code> to verify assumptions about alignment, packing (e.g., when reading structs from original game files), or template restrictions
* Avoid Yoda conditionals &mdash; write <code>var == 1</code>, not <code>1 == var</code>
== Comments ==
* Prefer <code>//</code> for single-line comments
* Multi-line <code>/* */</code> comments: align the <code>*</code> on each intermediate line
* Use <code>//TODO:</code> and <code>//FIXME:</code> markers to flag work-in-progress or known issues
* Do not comment for the sake of it &mdash; if the code can be made clearer, prefer that over adding a comment


== Logging ==
== Logging ==
Line 295: Line 318:
* Local headers use paths '''relative to the OpenApoc root''': <code>"framework/logger.h"</code>, not <code>"../logger.h"</code> or <code>"logger.h"</code>
* Local headers use paths '''relative to the OpenApoc root''': <code>"framework/logger.h"</code>, not <code>"../logger.h"</code> or <code>"logger.h"</code>
* '''Prefer forward declarations''' over <code>#include</code> in headers where possible
* '''Prefer forward declarations''' over <code>#include</code> in headers where possible
** Forward declarations are '''not possible''' for templates, non-class types (e.g., <code>enum class</code>), and superclasses. When in doubt, try building without the include and see what fails.


<pre>
<pre>

Latest revision as of 23:05, 1 March 2026

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