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やコンテナ参照に置き換える。 - 所有権と検証に必要な情報を一貫して管理する仕組みを採用する。