Coding Convention: Difference between revisions

From NCOT Wiki
Jump to navigationJump to search
No edit summary
 
Line 380: Line 380:
Organize function parameters in this recommended order:
Organize function parameters in this recommended order:


# '''Out parameters''' – pointers the function will write to
# '''Handles or resource identifiers''' – e.g., file handles, object IDs
# '''Handles or resource identifiers''' – e.g., file handles, object IDs
# '''Main input data''' – e.g., buffers, filenames, strings
# '''Main input data''' – e.g., buffers, filenames, strings
# '''Modifiers or options''' – flags, enums, or mode settings
# '''Modifiers or options''' – flags, enums, or mode settings
# '''Callbacks and user data''' – for event-driven or iterator-style functions
# '''Callbacks and user data''' – for event-driven or iterator-style functions
# '''Out parameters''' – pointers the function will write to


==== Examples ====
==== Examples ====


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
int load_data(Data *out, const char *filename);
int load_data(const char *filename, Data *out);
int get_size(int *out_size, const Object *obj);
int get_size(const Object *obj, int *out_size);
int encode(uint8_t *out, size_t out_size, const char *text);
int encode(const char *text, uint8_t *out, size_t out_size);
int run_query(const char *query, ResultFn callback, void *user_data);
int run_query(const char *query, ResultFn callback, void *user_data);
</syntaxhighlight>
</syntaxhighlight>


==== Notes ====
==== Notes ====
* Always place "out" parameters first for clarity.
* Use const for all inputs that do not change within the function
* Always place "out" parameters last for clarity.
* Group related parameters (e.g. buffer + size).
* Group related parameters (e.g. buffer + size).
* Place configuration flags after core inputs.
* Place configuration flags after core inputs.

Latest revision as of 13:51, 29 July 2025

C Coding Convention

This convention defines naming and style rules for writing clean, consistent, and self-documenting C code.

General Principles

  • Use lowercase_with_underscores for variables and functions.
  • Use PascalCase for struct and enum type names.
  • Use prefixes to namespace functions and globals by module/library.
  • Keep names brief but descriptive.

1. Enums

Type Name

  • Use PascalCase with an `_e` suffix.
typedef enum GameState_e {
    GAME_STATE_INIT,
    GAME_STATE_RUNNING,
    GAME_STATE_PAUSED,
    GAME_STATE_EXIT
} GameState_e;

Enum Values

  • Use ALL_CAPS with a common prefix for namespacing.

2. Structs / "Classes"

Type Name

  • Use PascalCase with a `_t` suffix.
typedef struct Player_t {
    int id;
    char name[32];
    float health;
} Player_t;

Member Names

  • Use lowercase_with_underscores.
  • Optionally prefix with struct abbreviation if needed (e.g. `pos_x`, `player_name`).

3. Global Variables

  • Use `g_` prefix.
  • Follow with lowercase_with_underscores.
int g_frame_counter;
bool g_is_debug_mode;
  • For internal linkage, use `static`:
static int g_last_error_code;

4. Functions

General Functions

  • Use lowercase_with_underscores.
  • Use verb_noun structure for clarity.
void init_game();
int calculate_damage(int weapon_id);

Library/Module Functions

  • Prefix with the module name (e.g. `audio_`, `player_`).
// In audio module
void audio_init();
void audio_play_sound(const char *name);

5. Constants / Macros

  • Use ALL_CAPS_WITH_UNDERSCORES.
#define MAX_PLAYERS 16
#define PI 3.14159f

6. File Naming

  • Use `module_name.c` and `module_name.h`.

Examples:

  • `player.c`, `game_state.h`, `audio_mixer.c`

7. Example Summary

// game_state.h
typedef enum GameState_e {
    GAME_STATE_INIT,
    GAME_STATE_RUNNING,
    GAME_STATE_PAUSED,
    GAME_STATE_EXIT
} GameState_e;

// player.h
typedef struct Player_t {
    int id;
    char name[32];
    float health;
    bool is_alive;
} Player_t;

// player.c
Player_t g_main_player;

void player_init(Player_t *player);
void player_take_damage(Player_t *player, float damage);

8. Return Value Guidelines

Well-defined return values make code more predictable and easier to debug. Follow these conventions:

8.1 Function Types

**Void functions**

Use when the function performs an action and no result needs to be reported.

void log_message(const char *msg);
**Value-returning functions**

Use to return computed values or status.

int calculate_score(int time_remaining, int enemies_defeated);

8.2 Return Codes for Status/Errors

**Success/Failure convention**

Return `0` for success, non-zero for failure. Use named `#define` or `enum` values for clarity.

// error_codes.h
#define ERR_OK          0
#define ERR_FILE_NOT_FOUND 1
#define ERR_INVALID_ARG     2
// file_loader.c
int file_load(const char *path) {
    if (!path) return ERR_INVALID_ARG;
    ...
    return ERR_OK;
}
**Boolean results**

Return `bool` if the result is true/false only. Use `<stdbool.h>`.

#include <stdbool.h>

bool player_is_alive(Player_t *player);

8.3 Output via Pointers

If a function needs to return multiple values, return one as the return value and others via pointers.

bool get_player_stats(int player_id, int *health_out, int *score_out);

Use `_out` or `_result` suffix for output parameters.

8.4 Document Return Meaning Clearly

Always document what return values mean in headers or comments.

/**
 * Loads a resource from disk.
 * 
 * @param path Path to the resource file.
 * @return ERR_OK on success, or an error code on failure.
 */
int resource_load(const char *path);

8.5 Optional Return Value Idioms

**Nullable pointers**

Return `NULL` on failure when returning pointers.

Texture_t *texture_load(const char *filename); // Returns NULL on failure
**Sentinel values**

Use a known invalid value (e.g. `-1`, `NULL`, `0`) if needed and document it clearly.

int find_entity_by_name(const char *name); // Returns -1 if not found

9. When to Use a Struct

Structs in C are used to group related data together into a single unit. Use them to improve clarity, reduce duplication, and logically organize your code.

9.1 Use a Struct When

  • You have related data items that conceptually belong together.
// Good: Position is a single concept with multiple parts
typedef struct Position_t {
    int x;
    int y;
} Position_t;
  • A function takes or returns multiple values that are always used together.
// Instead of passing x and y separately:
void move_to(int x, int y);

// Use:
void move_to(Position_t pos);
  • You want to encapsulate the state of a module, object, or entity.
typedef struct Player_t {
    int id;
    char name[32];
    int health;
    int score;
} Player_t;
  • You want to make your code more self-documenting.
 Structs make data structures explicit and easier to reason about.
  • You are modeling a real-world object or concept.
 For example: `Window_t`, `Vector2D_t`, `Enemy_t`, `Timer_t`, etc.

9.2 Avoid Creating a Struct When

  • The values are only used temporarily and locally in one function.
 In this case, keep them as local variables unless clarity improves by grouping.
  • The data items are not conceptually related.
 Grouping unrelated values can reduce clarity.
  • It will add unnecessary indirection or complexity.
 Avoid premature abstraction — keep it simple unless there's a clear benefit.

9.3 Naming Guidance

  • Use `PascalCase` + `_t` for struct type names.
  • Use `lowercase_with_underscores` for member fields.
  • Prefer descriptive member names over short or cryptic ones.

9.4 Example: Before and After Struct

Before (repetitive, unclear):

void render_entity(int x, int y, int width, int height, const char *texture);

After (clear, modular):

typedef struct Rect_t {
    int x;
    int y;
    int width;
    int height;
} Rect_t;

void render_entity(Rect_t bounds, const char *texture);

10. When to Split a Function

Long functions are not inherently bad. Sometimes, a complex process is best represented as a single cohesive unit. However, splitting functions **can improve clarity, testability, and reduce errors** — when done for the right reasons.

10.1 Good Reasons to Split a Function

  • The function performs multiple distinct steps or phases.

Break into helper functions that express each logical phase clearly.

// Original (hard to follow)
void process_file(const char *path) {
    // Open file
    // Validate header
    // Read data
    // Process data
    // Close file
}

// Better
void process_file(const char *path) {
    FILE *f = open_file(path);
    if (!validate_header(f)) { ... }
    Data_t data = read_data(f);
    process_data(&data);
    fclose(f);
}
  • There’s a repeated block of logic that would benefit from naming.

Give it a name by turning it into a small function. This reduces duplication and errors.

// Repeated block
if (str[i] >= 'A' && str[i] <= 'Z') {
    str[i] = str[i] + ('a' - 'A');
}

// Better
char to_lower(char c);
  • A block of code can be given a clear, self-documenting name.

If naming a chunk of code would make the main function easier to read, extract it.

if (is_valid_email_format(email)) { ... }
  • You want to isolate error-prone logic.

Reducing the cognitive load of complex or sensitive logic (e.g., bitwise operations, memory management) makes it easier to verify correctness.

  • The function is hard to test as-is.

Small helper functions with clear inputs and outputs are easier to test independently.

  • You need to reuse part of the logic elsewhere.

Avoid copy-pasting; encapsulate reusable logic in a function.

10.2 When *Not* to Split a Function

  • The logic is cohesive and only makes sense together.

If splitting would make the flow harder to follow, don’t. Prefer keeping it together with clear internal structure (comments, spacing).

  • Splitting would add excessive indirection.

Too many tiny functions can scatter logic and make it harder to understand at a glance.

  • The helper function would be used only once and has no meaningful name.

If the code is short and straightforward, extracting it may obscure the logic rather than clarify it.

10.3 Techniques for Managing Complexity

  • Use inline comments to group related steps inside a longer function.
  • Use blank lines and visual structure to separate phases.
  • Use helper functions to hide unimportant detail and highlight "what" over "how".

10.4 Summary

A function should be split when:

  • It performs more than one conceptually distinct task.
  • A block of code deserves a name to explain its purpose.
  • You want to improve readability, reduce repetition, or isolate complexity.

But don’t split functions just to make them shorter — split them to make them clearer.

Function Parameter Ordering Guidelines

Organize function parameters in this recommended order:

  1. Handles or resource identifiers – e.g., file handles, object IDs
  2. Main input data – e.g., buffers, filenames, strings
  3. Modifiers or options – flags, enums, or mode settings
  4. Callbacks and user data – for event-driven or iterator-style functions
  5. Out parameters – pointers the function will write to

Examples

int load_data(const char *filename, Data *out);
int get_size(const Object *obj, int *out_size);
int encode(const char *text, uint8_t *out, size_t out_size);
int run_query(const char *query, ResultFn callback, void *user_data);

Notes

  • Use const for all inputs that do not change within the function
  • Always place "out" parameters last for clarity.
  • Group related parameters (e.g. buffer + size).
  • Place configuration flags after core inputs.
  • Use consistent naming and ordering across similar functions.

Using const in C

  1. Use const for function parameters that should not be modified by the function.
  2. Declare variables const if their value must remain fixed.
  3. Mark pointers const when the data pointed to must not be changed.
  4. Use const for strings and buffers that must remain read-only.
  5. Apply const to struct members that should not be modified after initialization.
  6. Return const pointers if you want to prevent modification of internal data.

Examples

void print_message(const char *msg);  // msg won't be modified

const int max_connections = 10;      // constant value

typedef struct {
    const char *name;                 // name is read-only
    int id;
} User;

const char *get_version(void);        // caller cannot modify returned string

Notes

  • Do not use const on output parameters (pointers where data is modified).
  • Use const to help the compiler catch unintended modifications early.

Common Mistakes with const

  1. Modifying const data causes compile errors or undefined behavior.
  2. Casting away const to modify data is unsafe and discouraged.
  3. Confusing pointer to const and const pointer is a frequent source of bugs.

Pointer const qualification

Declaration Meaning
const int *ptr; Pointer to const int (data read-only)
int * const ptr; Const pointer to int (pointer fixed)
const int * const ptr; Const pointer to const int

Using const with function pointers

// Function pointer with const parameter
typedef void (*PrintFn)(const char *msg);

void print_message(const char *msg) {
    printf("%s\n", msg);
}

void call_printer(PrintFn fn, const char *text) {
    fn(text);
}

// Function pointer returning const pointer
typedef const char *(*GetVersionFn)(void);

const char *get_version(void) {
    return "v1.0";
}

Notes

  • Function pointer types must exactly match const qualifiers of the functions assigned to them.
  • Use const in function pointers to clarify intent and prevent modification.

Common Mistakes with const

  1. Modifying const data causes compile errors or undefined behavior.
  2. Casting away const to modify data is unsafe and discouraged.
  3. Confusing pointer to const and const pointer is a frequent source of bugs.