コンテンツにスキップ

03.C言語 コーディング規約

このドキュメントでは、本プロジェクトでC言語を記述する際の、コーディングスタイルと規約について定めます。 C言語は自由度が高い反面、規約がないとコードの品質に大きな差が生まれるため、一貫したスタイルを保つことが極めて重要です。

Note

"共通原則との関係" 本規約は、01.共通コーディング原則 をC言語に特化・具体化したものです。必ず共通原則にも目を通してください。


1. 基本方針 (Guiding Principles)

  • 本プロジェクトのC言語コードは、安全性、移植性、可読性を、高品質なコードを支える三つの柱として位置づけ、常にこれらの最適なバランスを追求します。
  • 可能な限り、C99以降の標準に準拠し、その機能を活用することを推奨します。

Tip

"原則が衝突する場合の指針" 稀に、これらの原則が互いに衝突する場合があります(例: 高い移植性を維持するためのコードが、特定の環境での最高の安全性や可読性を少し損なうケース)。 そのような場合は、独断で判断せず、チームで議論することを推奨します。最終的に何らかのトレードオフを受け入れる決定をした際は、なぜその選択をしたのかをコードコメントに明記し、 将来の保守者がその設計意図を理解できるようにしてください。


2. レイアウトと書式設定 (Layout and Formatting)

手作業でのスタイル遵守は非効率であり、レビューのノイズとなるため、本プロジェクトではツールによる規約の遵守を強制します。各ツールの設定は、リポジトリのルートに配置された .clang-format ファイル(フォーマッター)および .clang-tidy ファイル(静的解析)で一元管理します。また、エディタレベルでの基本的な設定は .editorconfig ファイルで統一します。

  • フォーマッター: ClangFormat

    • 役割: プロジェクトのスタイルを定義した.clang-formatファイルに基づき、コードの見た目を自動で統一します。
    • 運用: VSCodeのC/C++拡張機能と連携し、ファイル保存時に自動でフォーマットを適用します。CI/CDでフォーマットが遵守されているかをチェックします。
  • 静的解析 (主): Clang-Tidy

    • 役割: Clangコンパイラ基盤の強力な静的解析ツールです。潜在的なバグ、パフォーマンスの問題、コーディングスタイルの違反に加え、C言語特有の危険なコーディングパターンや、規約への準拠など、非常に広範なチェックを行います。
    • 運用: プロジェクトの品質の主軸として利用します。CI/CDプロセスに組み込み、重大な警告はエラーとして扱うことを強く推奨します。
  • 静的解析 (補): Cppcheck

    • 役割: 誤検出が少ないことに定評のある、高速な静的解析ツールです。特にメモリリークやリソースリークといった、古典的だが重大なバグの発見に優れています。
    • 運用: Clang-Tidyを補完する「セカンドオピニオン」としてCI/CDプロセスに組み込みます。Clang-Tidyとは異なる観点からコードをチェックすることで、解析の網羅性を高めます。

!!! success "CI/CDによる自動チェック" GitHub Actionsのワークフローにclang-format --dry-run --Werrorclang-tidy、およびcppcheckを組み込むことで、フォーマット・コード品質・静的解析が規約に違反しているコードのマージを自動的にブロックします。重大な警告はエラーとして扱う設定を必須とします。


3. 命名規則 (Naming Conventions)

  • snake_case (スネークケース):
    • 関数名 (void my_function();)
    • 変数名 (int my_variable;)
  • PascalCase (パスカルケース):
    • 構造体 (struct)、共用体 (union)、enum の型定義名 (typedef)。
    • 例: typedef struct MyStruct { ... } MyStruct;
  • UPPER_SNAKE_CASE (大文字のスネークケース):
    • マクロ (#define) や enum のメンバー。
    • 例: #define MAX_BUFFER_SIZE 1024, enum Status { STATUS_OK, STATUS_ERROR };

4. コメント (Comments)

  • ドキュメントコメントの不使用:
    • 本プロジェクトでは、「仕様はDocs/フォルダに集約する」という原則に基づき、Doxygen形式のようなAPIドキュメント自動生成のためのコメントは原則として使用しません。
  • 通常のコメント (// または /* ... */):
    • コードが「何をしているか」よりも「なぜそうしているのか」という設計意図や背景、複雑なアルゴリズムの要点を説明するために使用します。
  • 機能IDとの連携:

    • 01.共通コーディング原則で定められた通り、機能の実装には、対応する機能IDをコメントとして明記します。
    // FUNC-AUTH-1-1: ユーザー名とパスワードで認証を行う
    int authenticate_user(const char* username, const char* password)
    {
        // なぜこの処理が必要か、という意図を記述...
        // ...
    }
    

5. 言語機能の利用方針 (Language Features)

5.1. C99/C11標準の活用 (Standard Features)

  • 固定幅整数型: intlong の代わりに、<stdint.h> で定義される int32_t, uint64_t などを積極的に利用し、ビット幅を明確にします。
  • bool型: <stdbool.h> をインクルードし、_Bool の代わりに bool, true, false を使用します。
  • forループ内での変数宣言: for (int i = 0; ...) のように、ループ変数のスコープを最小限に留めます。

5.2. ヘッダーファイル (Header Files)

  • インクルードガード: 全てのヘッダーファイルには、多重インクルードを防止するため、#pragma onceをファイルの先頭に記述することを強く推奨します。

    #pragma once
    
    // ... header content ...
    
    • #pragma onceが使えない古い環境との互換性が必要な場合に限り、伝統的なインクルードガードを使用します。
  • #includeの順序:

    • #includeディレクティブは、ファイルの先頭にまとめて配置し、以下の順序でグループ化します。この順序は、ヘッダーファイルの自己完結性を保証するのに役立ちます。
    • 対応するヘッダーファイル自身(例: my_module.cなら"my_module.h"
    • C標準ライブラリ (<stdio.h>, <stdlib.h>など)
    • 外部ライブラリのヘッダー
    • プロジェクト内の他のヘッダー

5.3. プリプロセッサとマクロ (Preprocessor and Macros)

  • 関数形式マクロの注意点:

    • マクロは型チェックを行わないため、可能な限り static inline 関数で代替することを検討してください。
    • マクロを使用する場合は、引数を必ず括弧 () で囲み、意図しない演算子の優先順位の問題を避けてください。
    • 複数行にわたるマクロは do { ... } while (0) で囲み、if文などの制御構文内で安全に使用できるようにします。
    // 悪い例
    #define SQUARE(x) x * x
    // SQUARE(a + b) は a + b * a + b と展開されてしまう
    
    // 良い例
    #define SQUARE(x) ((x) * (x))
    
    // 良い例 (複数行マクロ)
    #define LOG_ERROR(msg) do { \
        fprintf(stderr, "ERROR: %s\n", (msg)); \
    } while (0)
    
  • #if vs #ifdef:

    • 単なる定義の有無(On/Off)で切り替える場合は #ifdef を使用します。
    • 特定の値(0, 1など)で条件分岐する場合は #if を使用します。

6. エラー処理 (Error Handling)

C言語には例外機構がないため、エラーの発生を呼び出し元に確実に伝え、適切に処理するための統一された規約が極めて重要です。

6.1. 戻り値によるエラー通知 (Return Values)

  • 成功/失敗の通知:
    • 関数の処理が成功したか失敗したかを示す場合は、int型またはbool型(<stdbool.h>)を戻り値とすることを標準とします。
    • 成功時に 0 または true を、失敗時に負の値または false を返すように統一します。
  • エラー詳細の伝達:

    • エラーの詳細な理由(エラーコードなど)を伝える必要がある場合は、関数の引数として出力用のポインタを渡すパターンを推奨します。
    // FUNC-USER-1-2: 戻り値で成否を、出力引数で結果やエラーコードを返す
    bool get_user_data(int user_id, UserData* out_data, ErrorCode* out_error);
    

6.2. errno の利用 (Using errno)

  • <stdio.h><math.h>などの標準ライブラリ関数は、失敗時にグローバル変数 errno にエラーコードを設定します。
  • 利用時の注意点:

    • errno をチェックする前には、必ず errno = 0; のようにリセットしてください。 errno は成功時にクリアされないため、このリセットを怠ると、過去に発生した無関係なエラー値を誤って検出する可能性があります。
    • エラーメッセージの表示には、errnoに対応するエラー文字列を取得する strerror(errno) や、直接エラーメッセージを標準エラー出力に出力する perror() を活用してください。
    #include <stdio.h>
    #include <errno.h>
    #include <math.h>
    #include <string.h>
    
    void check_sqrt(double x)
    {
        // 1. 関数呼び出しの直前にerrnoをリセットする
        errno = 0;
    
        // 2. 関数を呼び出す
        double result = sqrt(x);
    
        // 3. errnoをチェックしてエラーを判断する
        if (errno != 0)
        {
            // perrorは現在のerrnoに基づいたエラーメッセージを出力する
            perror("sqrt failed");
    
            // strerrorはerrnoに対応するエラー文字列を返す
            // fprintf(stderr, "Error calculating square root: %s\n", strerror(errno));
        }
        else
        {
            printf("sqrt(%.2f) = %.2f\n", x, result);
        }
    }
    
    int main(void)
    {
        check_sqrt(4.0);  // 成功するケース
        check_sqrt(-1.0); // 失敗するケース (errnoにEDOMが設定される)
        check_sqrt(9.0);  // 成功するケース (リセットしないと、前回の-1.0のエラーを誤検出する可能性がある)
        return 0;
    }
    

6.3. カスタムエラーコード (Custom Error Codes)

  • プロジェクト固有のエラー状態を管理するために、enum を使ったカスタムエラーコード体系を定義することを強く推奨します。
  • これにより、エラーの種類を型安全かつ網羅的に扱うことができます。

6.4. 致命的なエラーの扱い (Handling Fatal Errors)

  • プログラムの前提条件が破壊されるような、回復不可能なエラー(例: NULLポインタが渡されるべきでない関数に渡された)を検出するため、assertマクロ (<assert.h>) を積極的に利用します。
  • assertは、デバッグビルド時(NDEBUGが未定義の場合)にのみ有効となり、リリースビルドでは無効化されるため、パフォーマンスへの影響はありません。

6.5. goto文によるクリーンアップ

  • 複数のリソース(メモリ、ファイルディスクリプタ等)を確保する関数において、エラー発生時の解放処理を1箇所にまとめる目的でのみ、goto文の使用を許可します。
  • これはLinuxカーネルなどでも見られる、C言語における一般的なエラー処理パターンです。

    int process_data(void)
    {
        void* resource1 = NULL;
        FILE* resource2 = NULL;
        int ret = -1; // Default to error
    
        resource1 = malloc(100);
        if (!resource1) {
            goto cleanup;
        }
    
        resource2 = fopen("data.txt", "r");
        if (!resource2) {
            goto cleanup;
        }
    
        // ... Normal processing ...
        ret = 0; // Success
    
    cleanup:
        if (resource2) {
            fclose(resource2);
        }
        if (resource1) {
            free(resource1);
        }
        return ret;
    }
    

7. 安全なコーディングプラクティス (Safe Coding Practices)

C言語の危険性

C言語は強力である一方、メモリ管理や未定義動作など、危険な落とし穴が多く存在します。安全なコードを書くためのプラクティスを徹底してください。

7.1. コンパイラ警告 (Compiler Warnings)

  • コンパイラの警告レベルは、可能な限り高く設定します。(GCC/Clang: -Wall -Wextra, MSVC: /W4 または /Wall
  • 全ての警告はエラーとして扱い (-Werror)、修正することを原則とします。 警告を無視してはいけません。

7.2. ポインターとメモリ管理 (Pointers and Memory Management)

  • ポインター変数は、宣言と同時に初期化します(NULLまたは有効なアドレスで)。初期化されていないポインターは非常に危険です。
  • malloc などで動的に確保したメモリは、不要になったら必ず free で解放します。誰がメモリの所有権を持つのかを常に意識して設計してください。

7.3. バッファオーバーフロー対策 (Buffer Overflow Protection)

  • strcpystrcatgets のような、バッファサイズを考慮しない危険な関数は使用を禁止します。
  • 代わりに、snprintfstrncpy などの、書き込むサイズを制限できる安全な関数を使用してください。

7.4. constの積極的な利用 (Use of const)

  • 変更されるべきでない変数やポインター引数には、積極的にconst修飾子を付け、意図しない変更をコンパイラに検出させます。

    // この関数はstrが指すメモリの内容を変更しないことを保証する
    void print_string(const char* str);
    

7.5. 未定義動作の回避 (Avoiding Undefined Behavior)

  • C言語の仕様で動作が保証されていない「未定義動作」に依存したコードは、コンパイラや環境によって予期せぬ挙動を引き起こすため、絶対に避けてください。
  • 具体例:
    • 符号付き整数のオーバーフロー: int max = INT_MAX; int result = max + 1;
    • NULLポインタのデリファレンス: int* p = NULL; *p = 10;
    • 解放済みメモリへのアクセス: free(p); *p = 10;

8. パフォーマンスに関する考慮事項 (Performance Considerations)

Warning

"早すぎる最適化は諸悪の根源" パフォーマンスの最適化は、プロファイリングによってボトルネックが特定された後に行うべきです。明確な根拠なく、可読性を犠牲にするような最適化は避けてください。

8.1. キーワードの活用

  • inline キーワード:
    • 頻繁に呼び出される小さな関数に対して inline を付与することで、コンパイラに関数呼び出しのオーバーヘッドを削減するためのインライン展開を推奨できます。ただし、最終的にインライン化するかどうかはコンパイラの判断に委ねられます。
  • restrict キーワード (C99):
    • ポインタ引数に対して restrict を付与することで、そのポインタが指すメモリ領域が、他のポインタからはアクセスされないことをコンパイラに伝えます。これにより、コンパイラはより積極的な最適化を行うことができます。

8.2. データアライメントとパディング

  • CPUは、特定のデータ型を、そのサイズの倍数となるメモリアドレスから読み込む方が高速です。
  • 構造体のメンバの順序によっては、コンパイラがアライメントを維持するために、パディング(隙間)を挿入することがあります。
  • パフォーマンスが重要な場面では、サイズの大きいメンバから順に定義することで、パディングを最小限に抑え、キャッシュ効率を高めることを意識してください。

9. 移植性と環境依存 (Portability and Environment Dependencies)

9.1. データ型とサイズ (Data Types and Sizes)

  • intlongといった基本データ型のサイズは、処理系によって異なります。(例: 64bit環境ではlongが64bitだが、32bit環境では32bit)
  • サイズの互換性が重要な場面(バイナリI/O、ネットワーク通信等)では、必ず <stdint.h> で定義される int32_tuint64_t のような固定幅整数型を使用してください。

9.2. エンディアン (Endianness)

  • マルチバイトのデータをメモリに格納する順序(バイトオーダー)には、ビッグエンディアンとリトルエンディアンの2種類があります。
  • ネットワーク通信(常にビッグエンディアン)や、異なるシステム間で共有されるバイナリファイルを扱う際は、エンディアンの違いを吸収する必要があります。
  • htons() (host to network short) や ntohl() (network to host long) といった標準的な関数を利用して、適切にバイトオーダーを変換してください。

9.3. 処理系定義の動作 (Implementation-Defined Behavior)

  • C言語の規格では、一部の動作が「処理系定義(implementation-defined)」とされています。これは、コンパイラによって挙動が異なる可能性があることを意味します。
  • char型が符号付き (signed) か符号なしか (unsigned) は、その代表例です。文字コードの範囲外の値をcharに格納すると、意図しない挙動を引き起こす可能性があります。文字データ以外を扱う場合は、signed charまたはunsigned charを明示的に使用してください。

10. その他 (Miscellaneous)

  • 不変性 (Immutability):
    • 変更されるべきでない変数やデータ構造に対しては、constを積極的に利用し、不変性を高めることを意識してください。
  • コメントアウトされたコードの禁止:
    • 不要になったコードは、コメントアウトして残さずに削除してください。コードの履歴はGitのバージョン管理システムで追跡します。