-
Notifications
You must be signed in to change notification settings - Fork 179
Coding Style and C Language Features
A coding style or formatting style generally refers to the organization, placement, and tab and brace formatting practices. That is, things that have no impact on the logical interpretation of the code and is only visual. You can get a more in-depth definition on wikipedia.
Currently there is no universal coding style enforced across the FSO codebase, but we do recommend that if you make changes in an existing file the existing style should be kept. The codebase contains code from the original Volition release (1999) through modern contributions, so styles vary significantly between files.
While not strictly enforced, these patterns are commonly followed:
Indentation: Most files use tabs for indentation. Some newer code uses spaces. Match the existing file.
Braces: Both K&R style and Allman style are present in the codebase. Match the surrounding code.
Line Length: No strict limit, but keeping lines reasonably short (around 120 characters) improves readability.
The codebase uses several naming conventions depending on the age and origin of the code:
| Element | Convention | Examples |
|---|---|---|
| Classes | PascalCase |
Executor, ShaderProgram, Action
|
| Structs (legacy) | snake_case with _t suffix |
opengl_shader_t, reinforcements
|
| Structs (modern) | PascalCase or snake_case |
FinalAction, opengl_vert_attrib
|
| Functions | snake_case |
opengl_shader_init(), stuff_string()
|
| Member functions | snake_case or camelCase |
getCamera(), any_set()
|
| Local variables | snake_case |
current_primary_bank, file_path
|
| Member variables (some newer code) |
m_ prefix |
m_workItems, m_enabled
|
| Constants/Macros | ALL_CAPS |
MAX_SHIP_SPARKS, PARSE_BUF_SIZE
|
| Enum classes | PascalCase |
ActionResult, CallbackResult
|
| Namespaces | lowercase |
executor, actions, util
|
The project requires C++11 as the language standard (set via CMAKE_CXX_STANDARD in cmake/Cxx11.cmake).
Minimum supported compilers (as of late 2025):
- Windows: Visual Studio 2022
- Linux: GCC 9 or Clang 16
- macOS: Apple Clang (latest Xcode)
All C++11 features are available. C++14 and C++17 features may work in practice since all CI compilers support them, but the official standard remains C++11. If you use features beyond C++11, be aware that this may change compatibility requirements.
RTTI may be used to dynamically query the type of an object at runtime.
All new code should use a comparison with nullptr instead of NULL (or treating the pointer as a boolean); in other words, if (ptr != nullptr) is preferred to if (ptr != NULL) or if (ptr).
The auto keyword is widely used throughout the codebase:
// Common patterns
auto it = container.find(key);
auto buffer = gr_get_uniform_buffer(...);
// Range-based for loops with auto
for (const auto& item : container) { ... }
for (auto& entry : vector_in) { ... }Use auto when the type is obvious from context or when dealing with complex iterator types. Prefer explicit types when clarity is important.
Preferred over traditional index-based loops when iterating over containers:
for (const auto& parameter : params) {
// process parameter
}
for (auto& db : Debris) {
// modify db
}Lambdas are commonly used, especially with STL algorithms:
auto it = std::find_if(container.begin(), container.end(),
[&](const Item& item) { return item.name == target_name; });
list.erase(std::remove_if(list.begin(), list.end(),
[&](int index) { return should_remove(index); }), list.end());
// Named lambdas for reuse
auto ADD_FLAG = [&](char id) { /* ... */ };Strongly-typed enums are preferred for new code:
enum class ActionResult {
Finished,
Errored,
Wait
};
enum class CycleDirection { NEXT, PREV };The project also provides a FLAG_LIST macro and flagset template for flag enumerations:
FLAG_LIST(Ship_Flags) {
Engines_on,
Disabled,
// ... more flags
NUM_VALUES
};
flagset<Ship_Flags> flags;
flags.set(Ship_Flags::Engines_on);
if (flags[Ship_Flags::Disabled]) { ... }Used for compile-time constants:
constexpr bool FSO_DEBUG = true;
constexpr size_t UBYTE_SIZE = 8;
const size_t INVALID_SIZE = static_cast<size_t>(-1);Move constructors and move assignment operators are used where appropriate:
opengl_shader_t(opengl_shader_t&& other) noexcept = default;
opengl_shader_t& operator=(opengl_shader_t&& other) noexcept = default;
// Explicitly deleted copy operations
opengl_shader_t(const opengl_shader_t&) = delete;
opengl_shader_t& operator=(const opengl_shader_t&) = delete;Used on functions where ignoring the return value is likely a bug:
[[nodiscard]] charT SCP_toupper(charT ch);FSO defines wrapper types around standard containers in globalincs/vmallocator.h:
| FSO Type | Standard Equivalent | Additional Features |
|---|---|---|
SCP_vector<T> |
std::vector<T> |
.contains(), .in_bounds()
|
SCP_string |
std::string |
- |
SCP_list<T> |
std::list<T> |
.contains() |
SCP_map<K,V> |
std::map<K,V> |
- |
SCP_set<T> |
std::set<T> |
.contains() |
SCP_unordered_map<K,V> |
std::unordered_map<K,V> |
- |
SCP_unordered_set<T> |
std::unordered_set<T> |
.contains() |
SCP_stringstream |
std::stringstream |
- |
SCP_queue<T> |
std::queue<T> |
- |
SCP_deque<T> |
std::deque<T> |
- |
Prefer using the SCP types in new code for consistency.
std::unique_ptr and std::shared_ptr are used throughout the codebase:
std::unique_ptr<Action> clone() const;
std::unique_ptr<opengl::ShaderProgram> program;The project provides make_unique and make_shared helper functions (in vmallocator.h) for C++11 compatibility:
auto ptr = make_unique<MyClass>(arg1, arg2);Use RAII for resource management. The util::finally helper provides scope-guard functionality:
#include "utils/finally.h"
void example() {
auto resource = acquire_resource();
auto cleanup = util::finally([&]() { release_resource(resource); });
// ... use resource ...
// cleanup runs automatically when scope exits
}Raw pointers are still common in legacy code, especially for objects managed by global arrays or game systems. When working with such code, follow the existing patterns.
FSO provides several assertion macros (defined in globalincs/pstypes.h and globalincs/toolchain/*.h):
| Macro | Description |
|---|---|
Assert(expr) |
Debug-only assertion. Compiled out in release builds. |
Assertion(expr, msg, ...) |
Debug-only assertion with a custom message. |
Verify(x) |
Assertion that runs in both debug and release builds. |
Assert(index >= 0);
Assertion(ptr != nullptr, "Pointer must not be null for %s", name);
Verify(file_handle != INVALID_HANDLE);For error conditions that should be reported to users (defined in osapi/dialogs.h):
| Function | Description |
|---|---|
Error(LOCATION, fmt, ...) |
Fatal error - terminates the program |
Warning(LOCATION, fmt, ...) |
Non-fatal warning (debug builds only) |
WarningEx(LOCATION, fmt, ...) |
Warning variant |
ReleaseWarning(LOCATION, fmt, ...) |
Warning that also appears in release builds |
if (file_not_found) {
Error(LOCATION, "Could not find file: %s", filename);
}Defined in globalincs/pstypes.h. Note the double parentheses - these are macros that expand to function calls:
| Macro | Description |
|---|---|
mprintf((fmt, ...)) |
Simple debug output |
nprintf((id, fmt, ...)) |
Categorized output with a filter ID |
mprintf(("Loading shader: %s\n", shader_name));
nprintf(("AI", "Ship %s entering strafe position\n", ship_name));
nprintf(("Network", "Received packet type %d\n", packet_type));The id parameter in nprintf is a category string (e.g., "AI", "Network", "General") that can be used to filter log output.
-
Int3()- Triggers a breakpoint in debug builds -
LOCATION- Expands to__FILE__, __LINE__for error reporting -
SCP_UNUSED(x)- Suppresses unused variable warnings
Both styles are used in the codebase:
Traditional guards (common in legacy code):
#ifndef _SHIP_H
#define _SHIP_H
// ...
#endif // _SHIP_H#pragma once (preferred for new files):
#pragma once
// ...Some files use both for maximum compatibility:
#ifndef _OSAPI_DIALOGS_H
#define _OSAPI_DIALOGS_H
#pragma once
// ...
#endifWhile not strictly enforced, a common pattern is:
- Corresponding header file (for .cpp files)
- Project headers
- Standard library headers
- Third-party library headers
Use Doxygen-style comments for API documentation:
/**
* @brief Shows an error dialog.
* Only use this function if the program is in an unrecoverable state.
*
* @param filename The source code filename where this function was called
* @param line The source code line number where this function was called
* @param format The error message to display (a format string)
*/
void Error(const char* filename, int line, const char* format, ...);
/** Represents a point in 3d space.
*
* Note: this is a struct, not a class, so no member functions. */
typedef struct vec3d { ... } vec3d;Namespaces are used to organize related functionality:
namespace actions {
class Action { ... };
}
namespace executor {
class Executor { ... };
}
namespace os {
namespace dialogs {
void Error(...);
}
}Using declarations are sometimes placed at the end of headers for convenience:
using os::dialogs::Error;| Macro | Purpose |
|---|---|
LOCATION |
Expands to __FILE__, __LINE__
|
NOX(s) |
Marks a string as non-translatable |
XSTR(str, index) |
Localization macro |
CLAMP(x, min, max) |
Clamps x to [min, max] range |
MIN(a, b) / MAX(a, b)
|
Minimum/maximum (wraps std::min/max) |
TRUE / FALSE
|
Legacy boolean defines (use true/false in new code) |
- Exceptions: Generally avoided in game code due to performance concerns. Used in some subsystems (e.g., dialog exception classes).
- Multiple inheritance: Used sparingly. Prefer composition.
-
memset/memcpy/memmove: Debug builds include safety checks that these are only used on trivially copyable types.
Unit tests use Google Test framework and are built when FSO_BUILD_TESTS is enabled. Tests are located alongside the code they test or in dedicated test directories.