Skip to content

Coding Style and C Language Features

Daniel Cazares edited this page Dec 28, 2025 · 2 revisions

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.

Formatting Guidelines

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.

Naming Conventions

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

Usage of C++ Features

Language Standard

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.

Specific Language Features

RTTI and dynamic_cast

RTTI may be used to dynamically query the type of an object at runtime.

nullptr

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).

auto Keyword

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.

Range-Based For Loops

Preferred over traditional index-based loops when iterating over containers:

for (const auto& parameter : params) {
    // process parameter
}

for (auto& db : Debris) {
    // modify db
}

Lambda Expressions

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) { /* ... */ };

Enum Classes

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]) { ... }

constexpr

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 Semantics

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;

[[nodiscard]] Attribute

Used on functions where ignoring the return value is likely a bug:

[[nodiscard]] charT SCP_toupper(charT ch);

Project-Specific Types and Containers

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.

Memory Management

Smart Pointers

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);

RAII Patterns

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

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.

Error Handling and Debugging

Assertions

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);

Error and Warning Functions

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);
}

Debug Logging

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.

Debug Macros

  • Int3() - Triggers a breakpoint in debug builds
  • LOCATION - Expands to __FILE__, __LINE__ for error reporting
  • SCP_UNUSED(x) - Suppresses unused variable warnings

Header Files

Include Guards

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
// ...
#endif

Include Order

While not strictly enforced, a common pattern is:

  1. Corresponding header file (for .cpp files)
  2. Project headers
  3. Standard library headers
  4. Third-party library headers

Documentation Comments

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

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;

Common Macros and Defines

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)

Features to Use with Caution

  • 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.

Testing

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.

Clone this wiki locally