C++ コアガイドライン:現代 C++ 開発における哲学と基本原則

C++ コアガイドラインの概要

C++ コアガイドラインは、Bjarne Stroustrup や Herb Sutter といった C++ の創成期に関わった専門家たちによって策定された、現代 C++ におけるコーディング規則とベストプラクティスの集合体です。このガイドラインは、インターフェース設計、リソース管理、メモリ安全性、並行処理といった高レベルな課題に焦点を当てています。これらの指針に従うことで、静的な型安全性を高め、リソースリークや一般的なバグを防止し、より高速で堅牢なソフトウェア構築が可能になります。

なお、本ガイドラインでは GSL(Guidelines Support Library)というサポートライブラリの利用を推奨しており、境界チェックや所有権の明示などに役立ちます。

P.1:アイデアはコードで直接表現する

コメントは嘘をつく可能性がありますが、コードは実行された通りに振る舞います。コンパイラはコメントを無視しますが、コードの semantics(意味論)はチェック可能です。したがって、可能な限りコード自体が意図を明確に伝えるべきです。

コード例:型による意図の明確化

class Color {
public:
    Priority get_priority() const;  // 👍 戻り値の型が意図を伝える
    int get_priority();             // 👎 整数に戻すだけでは意図が不明確
};

最初のメソッドは戻り値が Priority 型であることを明示しており、状態を変更しないことも分かります。2 番目のメソッドでは、戻り値の整数が何を意味するのか開発者が推測する必要があり、バグの原因となり得ます。

コード例:標準ライブラリの活用

言語の primitive な機能だけで実装すると、コードが冗長になり意図が埋もれてしまいます。

void search_data(std::list<int>& data)
{
    int target;
    std::cin >> target;

    // 👎 手動でのインデックス管理
    int found_index = -1;
    int i = 0;
    for (auto it = data.begin(); it != data.end(); ++it, ++i) {
        if (*it == target) {
            found_index = i;
            break;
        }
    }
}

同じ処理を標準アルゴリズムを使用することで、何を行っているかが明確になります。

void search_data(std::list<int>& data)
{
    int target;
    std::cin >> target;
    auto it = std::find(data.begin(), data.end(), target);  // 👍 意図が明確
}

開発者は C++ 標準ライブラリやプロジェクト固有の基盤ライブラリ、そして GSL に慣れ親しむべきです。

コード例:単位と型の安全性

// 👎 引数 s の意味(絶対値か増分か)と単位が不明
void apply_thrust(double s);
apply_thrust(2.3);

型ラッパーや単位付きリテラルを使用することで、意味を明確にできます。

void apply_thrust(Thrust t);    // 👍 型で意味を定義
apply_thrust(2.3);              // エラー:単位がない
apply_thrust(23_N / 10s);       // 👍 単位付き(N/s)

チェックポイント

  • オブジェクトやパラメータを変更しない場合は const を使用し、意図を固定する。
  • キャストは型チェックを bypass するため、設計上の問題を示唆していることが多い。避けるべき。
  • 手動のループ処理は、標準ライブラリのアルゴリズムで置き換えられないか検討する。

P.2:ISO 標準準拠の C++ コードを書く

このガイドラインは ISO 標準の C++ を対象としています。特定のプラットフォーム依存機能が必要な場合は、その使用範囲を最小限に抑え、インターフェースで抽象化すべきです。

  • 標準外拡張(例:#pragma once)は、環境によって挙動が異なる可能性がある。
  • 拡張機能は移植性を損なう。
  • 未定義動作(Undefined Behavior)を避ける。例えば、式の評価順序は保証されていない。
// 👎 foo() と bar() の呼び出し順序は未定義
int result = foo() * bar();

また、「実装定義」の動作(例:int の正確なサイズ)に依存しないように注意が必要です。特定の業界(自動車、航空など)では、動的メモリ確保や例外の使用を制限するルールがある場合もあります。

チェックポイント

  • 最新のコンパイラを使用し、拡張機能を無効化するオプションを設定する。

P.3:意図を表現する

コードの命名や構造を通じて、その処理が「何をするものか」を明確にすべきです。単に「どう実現するか」だけでなく、「何を達成したいか」をコードに反映させます。

コード例:ループの表現

gsl::index i = 0;
while (i < container.size()) {
    // ... container[i] を処理 ...
}

この写法では、インデックス変数 i の寿命がループ外まで伸びており、誤用されるリスクがあります。範囲ベース for ループを使用すると、イテレーションの機構を隠蔽できます。

for (const auto& item : container) {
    // ... item を処理 ...
}

変更が必要な場合は非 const 参照を使用します。さらに、標準アルゴリズム(for_each など)を使うことで、要素の処理順序に依存しない意図も伝えられます。

コード例:インターフェースの明確化

// 👎 引数の意味が文档を読まないと分からない
// (x, y, 半径) か、(x1, y1, x2, y2) か?
void render_circle(int, int, int);

// 👍 構造体で意味をまとめる
void render_circle(Position pos, Radius r);

チェックポイント

  • 単純なインデックスループは範囲ベース for に置き換える。
  • T* と int のペアは span<T> に置き換える。
  • 裸の new/delete は避ける。
  • 引数が多すぎる関数は重构する。

P.4:プログラムは静的に型安全であるべき

理想としては、コンパイラがすべての型エラーを编译時に検出できる状態(静的型安全性)を目指します。しかし、以下の要素により完全な達成は困難です。

  • union の使用
  • 明示的な型キャスト
  • 配列のポインタへの減衰
  • 範囲外アクセス
  • 窄化変換(Narrowing conversion)

これらはクラッシュやセキュリティ脆弱性の主要原因となります。

チェックポイント

  • union の代わりに std::variant (C++17) を使用する。
  • キャストは避け、テンプレートなどで解決する。
  • 配列の減衰を防ぐために span (GSL) を使用する。
  • 窄化変換には narrow_cast などを使用し、意図を明示する。

P.5:実行時チェックより编译時チェックを優先する

実行時エラーハンドリングはコードを複雑にし、パフォーマンスを低下させます。编译時に検証可能なことは、その段階で行うべきです。

コード例:サイズ検証

// 👎 実行時にビット数を数えて確認
int bits = 0;
for (Type i = 1; i; i <<= 1)
    ++bits;
if (bits < 32)
    std::cerr << "Type too small\n";

これは未定義動作を含む可能性があり、非効率的です。static_assert を使用します。

// 👍 编译時アサーション
static_assert(sizeof(Type) >= 4);

あるいは、int32_t のようにサイズが保証された型を直接使用します。

コード例:バッファ読み込み

// 👎 ポインタとサイズを別々に渡すと境界チェックが困難
void load_buffer(int* p, int n);
int arr[100];
load_buffer(arr, 1000); // オーバーフロー

span を使用することで、コンパイラが要素数を推論できるようになります。

void load_buffer(gsl::span<int> s);
int arr[100];
load_buffer(arr); // 👍 要素数が自動的に計算される

P.6:编译時チェック不可能なことは実行時チェック可能にする

すべてのエラーを编译時に捕まえることは不可能ですが、実行時にも検出可能な設計にする必要があります。特に、所有権とサイズ情報が分離している場合はエラー検出が困難になります。

コード例:所有権とサイズの分離

// 👎 サイズ情報が関数に渡されていない
extern void process(int* p);
void caller(int n) {
    process(new int[n]); // n の情報が process 内で不明
}

サイズを別途渡す方法もありますが、人間によるミス(変数の取り違えなど)が発生しやすくなります。

// 👎 サイズとポインタが別引数
extern void process2(int* p, int n);
void caller2(int n) {
    process2(new int[n], m); // n と m を間違える可能性
}

スマートポインタを使用しても、配列のサイズ情報は保持されません。

// 👎 unique_ptr ではサイズ管理が明示的ではない
extern void process3(std::unique_ptr<int[]>, int n);

コード例:オブジェクトとしての管理

コンテナクラスを使用することで、サイズ情報をオブジェクトの一部として保持できます。

extern void process4(std::vector<int>&);
extern void process4(gsl::span<int>);

void caller3(int n) {
    std::vector<int> v(n);
    process4(v);            // 所有権を保持
    process4(gsl::span<int>{v}); // 视图として渡す
}

所有権を移動させる場合も、コンテナを返す方が安全です。

// 👍 vector を返すことでサイズ情報も一緒に移動
std::vector<int> create_data(int n) {
    std::vector<int> v(n);
    // ... 初期化 ...
    return v;
}

// 👎 裸のポインタや unique_ptr<T[]> はサイズ情報を失う
std::unique_ptr<int[]> create_raw(int n) { ... }

チェックポイント

  • ポインタとサイズのペアを渡すインターフェースは、span やコンテナ参照に置き換える。
  • 所有権と検証に必要な情報を一貫して管理する仕組みを採用する。

タグ: cpp-core-guidelines modern-cpp type-safety resource-management gsl

9月23日 13:28 投稿