Coding Style (OpenApoc)
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:/;infor
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();
}
switchalways has adefaultcase unless switching over anenum classwhere every value is handled- All
casesections should have abreak casebraces{}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;
}
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 |
Types
- Use
autoliberally, especially when the type is obvious from the right-hand side. Useauto &to avoid copies. - Use
enum classover plainenum - Use
structonly for data-only types; structs must never use access modifiers - No C-style casts — use
static_cast<>,dynamic_cast<>,reinterpret_cast<> - Prefer
{}brace initialization
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) oversp<>unless shared ownership is genuinely needed - Use
std::move()to transferup<>ownership
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 virtualonly on the base class;overrideon derived classes — never both together- All classes with virtual methods must have a virtual destructor
- Use
= defaultinstead 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 constaggressively: const member functions, const parameters, const return types, const local variables- Range-for loops:
for (auto &element : container)/for (const auto &element : container) - Prefer
emplace()overinsert()in STL containers
General Code
- Use anonymous namespaces instead of
staticfor 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
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 informationLogWarning— recoverable errorsLogError— 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
#includein headers where possible
#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:
- const correctness — if something can be
const, it must beconst - Readable conditionals — avoid embedded comments in complex conditionals; prefer early-exit checks
- One logical change per PR — keep changes focused for clean history and bisectability
- Use
auto— when the type is already visible on the RHS - Prefer exclusive ownership — use
up<>oversp<>when shared ownership is not required
See Also
- OpenApoc — Main OpenApoc page
- Compiling (OpenApoc) — Build instructions for developers
- CODE_STYLE.md — Full coding style specification in the repository
- OpenApoc Wiki: Coding Style — Extended coding style documentation
- OpenApoc on GitHub — Source code repository
- OpenApoc Discord — Community discussion and support